<commit_before>// Dependencies
var transformImage = window.transformImage;
var saveAs = window.saveAs;
var ImageMarkers = window.ImageMarkers;

function CropComponent(options) {
    this.width = options.width;
    this.height = options.height;
    this.enableFilePicker = options.enableFilePicker;
    this.enableDownload = options.enableDownload;
    this.enableUpload = options.enableUpload;
    this.enableMarkers = options.enableMarkers;
    this.maxMarkerCount = options.maxMarkerCount || 0;
    this.uploadURL = options.uploadURL;

    this.baseScale = 1;
    this.scale = 1;
    this.rotation = 0;
    this.offsetX = 0;
    this.offsetY = 0;
    this.imageFilename = '';
    this.imageBlobURL = '';
    this.markers = new ImageMarkers(this.enableMarkers ? this.maxMarkerCount : 0);

    this.dragging = false;
    this.dragStartTime = 0;
    this.dragStartPointerX = 0;
    this.dragStartPointerY = 0;
    this.dragStartOffsetX = 0;
    this.dragStartOffsetY = 0;
    this.dragPreviousX = 0;
    this.dragPreviousY = 0;
    this.dragDistance = 0;

    this.initDOM();
    this.initDOMEvents();

    if (options.imageURL) {
        this.setImage(options.imageURL, options.imageURL);
    }
}

CropComponent.prototype.getDOMNode = function() {
    return this.node;
};

CropComponent.prototype.initDOM = function() {
    var root = document.createElement('div');
    root.className = 'crop-component';
    root.innerHTML =
        '
' +
            '' +
        '
' +
        '
' +
            '
' +
                '<button class="crop-component-faux-input-file">Choose image</button>' +
                '<input class="crop-component-input-file" type="file" accept="image/*">' +
            '
' +
            '
' +
                'Rotate' +
                '<input class="crop-component-input-rotate" type="range" min="-1" max="1" step="0.001" value="0">' +
            '
' +
            '
' +
                'Scale' +
                '<input class="crop-component-input-scale" type="range" min="0" max="1" step="0.001" value="0">' +
            '
' +
            '
' +
                '<button class="crop-component-download">Download</button>' +
                '<button class="crop-component-upload">Upload</button>' +
            '
' +
        '
';

    this.node = root;
    this.imageBoxNode = this.node.querySelector('.crop-component-image-box');
    this.imageNode = this.node.querySelector('.crop-component-image');
    this.filePickerSectionNode = this.node.querySelector('.crop-component-control-file-picker');
    this.fauxInputFileNode = this.node.querySelector('.crop-component-faux-input-file');
    this.inputFileNode = this.node.querySelector('.crop-component-input-file');
    this.inputRotateNode = this.node.querySelector('.crop-component-input-rotate');
    this.inputScaleNode = this.node.querySelector('.crop-component-input-scale');
    this.downloadButtonNode = this.node.querySelector('.crop-component-download');
    this.uploadButtonNode = this.node.querySelector('.crop-component-upload');

    this.node.style.width = this.width + 'px';
    this.imageBoxNode.style.width = this.width + 'px';
    this.imageBoxNode.style.height = this.height + 'px';

    this.filePickerSectionNode.style.display = this.enableFilePicker ? '' : 'none';
    this.downloadButtonNode.style.display = this.enableDownload ? '' : 'none';
    this.uploadButtonNode.style.display = this.enableUpload ? '' : 'none';

    this.imageBoxNode.appendChild(this.markers.getDOMNode());
};

CropComponent.prototype.initDOMEvents = function() {
    this.fauxInputFileNode.addEventListener('click',
        this.inputFileNode.click.bind(this.inputFileNode));

    this.inputFileNode.addEventListener('change', this.onFileSelect.bind(this));
    this.inputRotateNode.addEventListener('input', this.onRotationChange.bind(this));
    this.inputScaleNode.addEventListener('input', this.onScaleChange.bind(this));
    this.inputRotateNode.addEventListener('change', this.onRotationChange.bind(this)); // IE
    this.inputScaleNode.addEventListener('change', this.onScaleChange.bind(this)); // IE

    this.imageBoxNode.addEventListener('mousedown', this.onDragStart.bind(this));
    document.addEventListener('mousemove', this.onDragMove.bind(this));
    document.addEventListener('mouseup', this.onDragEnd.bind(this));

    this.downloadButtonNode.addEventListener('click', this.download.bind(this));
    this.uploadButtonNode.addEventListener('click', this.upload.bind(this));
};

CropComponent.prototype.setImage = function(imageURL, imageFilename) {
    this.imageNode.onload = function() {
        this.node.classList.add('crop-component-image-selected');
        this.baseScale = Math.min(
            1, // don't upscale tiny images
            this.width  / this.imageNode.naturalWidth,
            this.height / this.imageNode.naturalHeight
        );
    }.bind(this);
    this.imageFilename = imageFilename;
    this.imageNode.src = imageURL;
    this.resetControls();
    this.resetTransform();
    this.applyTransform();
};

CropComponent.prototype.onFileSelect = function(event) {
    if (this.imageBlobURL && URL.revokeObjectURL) {
        URL.revokeObjectURL(this.imageBlobURL);
    }

    this.imageBlobURL = URL.createObjectURL(event.target.files[0]);
    this.setImage(this.imageBlobURL, event.target.value);
};

CropComponent.prototype.resetControls = function() {
    this.inputRotateNode.value = this.inputRotateNode.defaultValue;
    this.inputScaleNode.value = this.inputScaleNode.defaultValue;
};

CropComponent.prototype.resetTransform = function() {
    this.scale = 1;
    this.rotation = 0;
    this.offsetX = 0;
    this.offsetY = 0;
};

CropComponent.prototype.getImageBoundingBox = function() {
    // Coordinates of the bottom right corner of the image relative to its
    // center before rotation around the image center.
    var x = this.imageNode.naturalWidth  * this.baseScale * this.scale / 2;
    var y = this.imageNode.naturalHeight * this.baseScale * this.scale / 2;

    // Coordinates of the top right and bottom right corners after rotation.
    var a = this.rotation * Math.PI;
    var sinA = Math.sin(a);
    var cosA = Math.cos(a);
    var x1 = x * cosA - y * sinA;
    var y1 = x * sinA + y * cosA;
    var x2 = x * cosA + y * sinA;
    var y2 = x * sinA - y * cosA;

    // The remaining two corners are simply reflections of the first two.
    return {
        width:  Math.max(x1, x2, -x1, -x2) - Math.min(x1, x2, -x1, -x2),
        height: Math.max(y1, y2, -y1, -y2) - Math.min(y1, y2, -y1, -y2)
    };
};

CropComponent.prototype.applyTransformConstraints = function() {
    // If the image bounding box is leaving the cropped area, push it back.

    var imageAABB = this.getImageBoundingBox();
    var overflowX = Math.max(0, imageAABB.width  - this.width);
    var overflowY = Math.max(0, imageAABB.height - this.height);

    this.offsetX = Math.min(this.offsetX,  overflowX / 2);
    this.offsetX = Math.max(this.offsetX, -overflowX / 2);
    this.offsetY = Math.min(this.offsetY,  overflowY / 2);
    this.offsetY = Math.max(this.offsetY, -overflowY / 2);
};

CropComponent.prototype.applyTransform = function() {
    this.imageNode.style.transform =
        'translate(' + this.offsetX + 'px, ' + this.offsetY + 'px) ' +
        'rotate(' + 180 * this.rotation + 'deg) ' +
        'scale(' + this.scale + ', ' + this.scale + ')';
};

CropComponent.prototype.updateTransform = function() {
    this.applyTransformConstraints();
    this.applyTransform();
};

CropComponent.prototype.onDragStart = function(event) {
    if (event.which === 1 && !this.markers.wantToHandle(event)) {
        event.preventDefault(); // block native dragging and text selection
        this.dragging = true;
        this.dragStartTime = Date.now();
        this.dragStartPointerX = event.pageX;
        this.dragStartPointerY = event.pageY;
        this.dragStartOffsetX = this.offsetX;
        this.dragStartOffsetY = this.offsetY;
        this.dragPreviousX = event.pageX;
        this.dragPreviousY = event.pageY;
        this.dragDistance = 0;
    }
};

CropComponent.prototype.onDragMove = function(event) {
    if (this.dragging) {
        this.offsetX = this.dragStartOffsetX + event.pageX - this.dragStartPointerX;
        this.offsetY = this.dragStartOffsetY + event.pageY - this.dragStartPointerY;
        this.updateTransform();

        this.dragDistance += Math.sqrt(
            Math.pow(event.pageX - this.dragPreviousX, 2),
            Math.pow(event.pageY - this.dragPreviousY, 2)
        );
        this.dragPreviousX = event.pageX;
        this.dragPreviousY = event.pageY;
    }
};

CropComponent.prototype.onDragEnd = function(event) {
    if (this.dragging && event.which === 1) {
        this.dragging = false;
        if (this.dragDistance < 4 && Date.now() - this.dragStartTime < 500) {
            this.markers.addAtScreenCoords(event.clientX, event.clientY);
        }
    }
};

CropComponent.prototype.onRotationChange = function() {
    var newRotation = this.inputRotateNode.value;

    // Rotate as if the transform origin is at the center of the cropped area.
    // Imagine putting a mark on the image at the center of the cropped area,
    // find out where the mark would end up if we rotated the image around its
    // center, compensate for that movement so that the mark stays in place.
    var angle = (newRotation - this.rotation) * Math.PI;
    var oldX = -this.offsetX;
    var oldY = -this.offsetY;
    var newX = oldX * Math.cos(angle) - oldY * Math.sin(angle);
    var newY = oldX * Math.sin(angle) + oldY * Math.cos(angle);
    this.offsetX -= newX - oldX;
    this.offsetY -= newY - oldY;

    this.rotation = newRotation;
    this.updateTransform();
};

CropComponent.prototype.onScaleChange = function() {
    var newScale = Math.pow(10, this.inputScaleNode.value);

    // Scale as if the transform origin is at the center of the cropped area.
    // Imagine putting a mark on the image at the center of the cropped area,
    // find out where the mark would end up if we scaled the image around its
    // center, compensate for that movement so that the mark stays in place.
    var oldX = -this.offsetX;
    var oldY = -this.offsetY;
    var newX = oldX * newScale / this.scale;
    var newY = oldY * newScale / this.scale;
    this.offsetX -= newX - oldX;
    this.offsetY -= newY - oldY;

    this.scale = newScale;
    this.updateTransform();
};

CropComponent.prototype.getTransformedImageFilename = function() {
    var filename = this.imageFilename;
    filename = filename.split(/[\/\\]/).pop(); // strip path
    filename = filename.replace(/\.[^.]*$/g, ''); // strip extension
    filename = filename.trim() || 'image';
    return 'Processed ' + filename + '.jpg';
};

CropComponent.prototype.getTransformedImageBlob = function(callback) {
    var self = this;

    // Since transformImage can block, give some time for UI changes processing.
    setTimeout(function() {
        transformImage(
            self.imageNode,
            self.width, self.height,
            self.rotation * Math.PI,
            self.baseScale * self.scale,
            self.offsetX, self.offsetY,
            '#bebebe',
            function(canvas) {
                canvas.toBlob(callback, 'image/jpeg', 0.95);
            }
        );
    }, 32);
};

CropComponent.prototype.download = function() {
    var self = this;
    self.downloadButtonNode.disabled = true;

    self.getTransformedImageBlob(function(blob) {
        saveAs(blob, self.getTransformedImageFilename());
        self.downloadButtonNode.disabled = false;
    });
};

CropComponent.prototype.upload = function() {
    var self = this;
    self.uploadButtonNode.disabled = true;

    self.getTransformedImageBlob(function(blob) {
        var form = new FormData();
        form.append('image', blob, self.getTransformedImageFilename());
        self.markers.getPoints().forEach(function(point) {
            form.append('x[]', point.x);
            form.append('y[]', point.y);
        });

        var request = new XMLHttpRequest();
        request.open('POST', self.uploadURL);
        request.addEventListener('load', function() {
            // ...
        });
        request.addEventListener('error', function() {
            alert('Upload error');
        });
        request.addEventListener('abort', function() {
            // ...
        });
        request.addEventListener('loadend', function() {
            self.uploadButtonNode.disabled = false;
        });
        request.send(form);
    });
};
<commit_msg>Simplified transform computations<commit_after>// Dependencies
var transformImage = window.transformImage;
var saveAs = window.saveAs;
var ImageMarkers = window.ImageMarkers;

function CropComponent(options) {
    this.width = options.width;
    this.height = options.height;
    this.enableFilePicker = options.enableFilePicker;
    this.enableDownload = options.enableDownload;
    this.enableUpload = options.enableUpload;
    this.enableMarkers = options.enableMarkers;
    this.maxMarkerCount = options.maxMarkerCount || 0;
    this.uploadURL = options.uploadURL;

    this.baseScale = 1;
    this.scale = 1;
    this.rotation = 0;
    this.offsetX = 0;
    this.offsetY = 0;
    this.imageFilename = '';
    this.imageBlobURL = '';
    this.markers = new ImageMarkers(this.enableMarkers ? this.maxMarkerCount : 0);

    this.dragging = false;
    this.dragStartTime = 0;
    this.dragStartPointerX = 0;
    this.dragStartPointerY = 0;
    this.dragStartOffsetX = 0;
    this.dragStartOffsetY = 0;
    this.dragPreviousX = 0;
    this.dragPreviousY = 0;
    this.dragDistance = 0;

    this.initDOM();
    this.initDOMEvents();

    if (options.imageURL) {
        this.setImage(options.imageURL, options.imageURL);
    }
}

CropComponent.prototype.getDOMNode = function() {
    return this.node;
};

CropComponent.prototype.initDOM = function() {
    var root = document.createElement('div');
    root.className = 'crop-component';
    root.innerHTML =
        '
' +
            '' +
        '
' +
        '
' +
            '
' +
                '<button class="crop-component-faux-input-file">Choose image</button>' +
                '<input class="crop-component-input-file" type="file" accept="image/*">' +
            '
' +
            '
' +
                'Rotate' +
                '<input class="crop-component-input-rotate" type="range" min="-1" max="1" step="0.001" value="0">' +
            '
' +
            '
' +
                'Scale' +
                '<input class="crop-component-input-scale" type="range" min="0" max="1" step="0.001" value="0">' +
            '
' +
            '
' +
                '<button class="crop-component-download">Download</button>' +
                '<button class="crop-component-upload">Upload</button>' +
            '
' +
        '
';

    this.node = root;
    this.imageBoxNode = this.node.querySelector('.crop-component-image-box');
    this.imageNode = this.node.querySelector('.crop-component-image');
    this.filePickerSectionNode = this.node.querySelector('.crop-component-control-file-picker');
    this.fauxInputFileNode = this.node.querySelector('.crop-component-faux-input-file');
    this.inputFileNode = this.node.querySelector('.crop-component-input-file');
    this.inputRotateNode = this.node.querySelector('.crop-component-input-rotate');
    this.inputScaleNode = this.node.querySelector('.crop-component-input-scale');
    this.downloadButtonNode = this.node.querySelector('.crop-component-download');
    this.uploadButtonNode = this.node.querySelector('.crop-component-upload');

    this.node.style.width = this.width + 'px';
    this.imageBoxNode.style.width = this.width + 'px';
    this.imageBoxNode.style.height = this.height + 'px';

    this.filePickerSectionNode.style.display = this.enableFilePicker ? '' : 'none';
    this.downloadButtonNode.style.display = this.enableDownload ? '' : 'none';
    this.uploadButtonNode.style.display = this.enableUpload ? '' : 'none';

    this.imageBoxNode.appendChild(this.markers.getDOMNode());
};

CropComponent.prototype.initDOMEvents = function() {
    this.fauxInputFileNode.addEventListener('click',
        this.inputFileNode.click.bind(this.inputFileNode));

    this.inputFileNode.addEventListener('change', this.onFileSelect.bind(this));
    this.inputRotateNode.addEventListener('input', this.onRotationChange.bind(this));
    this.inputScaleNode.addEventListener('input', this.onScaleChange.bind(this));
    this.inputRotateNode.addEventListener('change', this.onRotationChange.bind(this)); // IE
    this.inputScaleNode.addEventListener('change', this.onScaleChange.bind(this)); // IE

    this.imageBoxNode.addEventListener('mousedown', this.onDragStart.bind(this));
    document.addEventListener('mousemove', this.onDragMove.bind(this));
    document.addEventListener('mouseup', this.onDragEnd.bind(this));

    this.downloadButtonNode.addEventListener('click', this.download.bind(this));
    this.uploadButtonNode.addEventListener('click', this.upload.bind(this));
};

CropComponent.prototype.setImage = function(imageURL, imageFilename) {
    this.imageNode.onload = function() {
        this.node.classList.add('crop-component-image-selected');
        this.baseScale = Math.min(
            1, // don't upscale tiny images
            this.width  / this.imageNode.naturalWidth,
            this.height / this.imageNode.naturalHeight
        );
    }.bind(this);
    this.imageFilename = imageFilename;
    this.imageNode.src = imageURL;
    this.resetControls();
    this.resetTransform();
    this.applyTransform();
};

CropComponent.prototype.onFileSelect = function(event) {
    if (this.imageBlobURL && URL.revokeObjectURL) {
        URL.revokeObjectURL(this.imageBlobURL);
    }

    this.imageBlobURL = URL.createObjectURL(event.target.files[0]);
    this.setImage(this.imageBlobURL, event.target.value);
};

CropComponent.prototype.resetControls = function() {
    this.inputRotateNode.value = this.inputRotateNode.defaultValue;
    this.inputScaleNode.value = this.inputScaleNode.defaultValue;
};

CropComponent.prototype.resetTransform = function() {
    this.scale = 1;
    this.rotation = 0;
    this.offsetX = 0;
    this.offsetY = 0;
};

CropComponent.prototype.getImageBoundingBox = function() {
    var w = this.imageNode.naturalWidth  * this.baseScale * this.scale;
    var h = this.imageNode.naturalHeight * this.baseScale * this.scale;
    var a = this.rotation * Math.PI;
    return {
        width:  Math.abs(w * Math.cos(a)) + Math.abs(h * Math.sin(a)),
        height: Math.abs(w * Math.sin(a)) + Math.abs(h * Math.cos(a))
    };
};

CropComponent.prototype.applyTransformConstraints = function() {
    // If the image bounding box is leaving the cropped area, push it back.

    var imageAABB = this.getImageBoundingBox();
    var overflowX = Math.max(0, imageAABB.width  - this.width);
    var overflowY = Math.max(0, imageAABB.height - this.height);

    this.offsetX = Math.min(this.offsetX,  overflowX / 2);
    this.offsetX = Math.max(this.offsetX, -overflowX / 2);
    this.offsetY = Math.min(this.offsetY,  overflowY / 2);
    this.offsetY = Math.max(this.offsetY, -overflowY / 2);
};

CropComponent.prototype.applyTransform = function() {
    this.imageNode.style.transform =
        'translate(' + this.offsetX + 'px, ' + this.offsetY + 'px) ' +
        'rotate(' + 180 * this.rotation + 'deg) ' +
        'scale(' + this.scale + ', ' + this.scale + ')';
};

CropComponent.prototype.updateTransform = function() {
    this.applyTransformConstraints();
    this.applyTransform();
};

CropComponent.prototype.onDragStart = function(event) {
    if (event.which === 1 && !this.markers.wantToHandle(event)) {
        event.preventDefault(); // block native dragging and text selection
        this.dragging = true;
        this.dragStartTime = Date.now();
        this.dragStartPointerX = event.pageX;
        this.dragStartPointerY = event.pageY;
        this.dragStartOffsetX = this.offsetX;
        this.dragStartOffsetY = this.offsetY;
        this.dragPreviousX = event.pageX;
        this.dragPreviousY = event.pageY;
        this.dragDistance = 0;
    }
};

CropComponent.prototype.onDragMove = function(event) {
    if (this.dragging) {
        this.offsetX = this.dragStartOffsetX + event.pageX - this.dragStartPointerX;
        this.offsetY = this.dragStartOffsetY + event.pageY - this.dragStartPointerY;
        this.updateTransform();

        this.dragDistance += Math.sqrt(
            Math.pow(event.pageX - this.dragPreviousX, 2),
            Math.pow(event.pageY - this.dragPreviousY, 2)
        );
        this.dragPreviousX = event.pageX;
        this.dragPreviousY = event.pageY;
    }
};

CropComponent.prototype.onDragEnd = function(event) {
    if (this.dragging && event.which === 1) {
        this.dragging = false;
        if (this.dragDistance < 4 && Date.now() - this.dragStartTime < 500) {
            this.markers.addAtScreenCoords(event.clientX, event.clientY);
        }
    }
};

CropComponent.prototype.onRotationChange = function() {
    var newRotation = this.inputRotateNode.value;

    // Rotate as if the transform origin is at the center of the box.
    var a = (newRotation - this.rotation) * Math.PI;
    var x = this.offsetX;
    var y = this.offsetY;
    this.offsetX = x * Math.cos(a) - y * Math.sin(a);
    this.offsetY = x * Math.sin(a) + y * Math.cos(a);

    this.rotation = newRotation;
    this.updateTransform();
};

CropComponent.prototype.onScaleChange = function() {
    var newScale = Math.pow(10, this.inputScaleNode.value);

    // Scale as if the transform origin is at the center of the box.
    this.offsetX *= newScale / this.scale;
    this.offsetY *= newScale / this.scale;

    this.scale = newScale;
    this.updateTransform();
};

CropComponent.prototype.getTransformedImageFilename = function() {
    var filename = this.imageFilename;
    filename = filename.split(/[\/\\]/).pop(); // strip path
    filename = filename.replace(/\.[^.]*$/g, ''); // strip extension
    filename = filename.trim() || 'image';
    return 'Processed ' + filename + '.jpg';
};

CropComponent.prototype.getTransformedImageBlob = function(callback) {
    var self = this;

    // Since transformImage can block, give UI some time to update.
    setTimeout(function() {
        transformImage(
            self.imageNode,
            self.width, self.height,
            self.rotation * Math.PI,
            self.baseScale * self.scale,
            self.offsetX, self.offsetY,
            '#bebebe',
            function(canvas) {
                canvas.toBlob(callback, 'image/jpeg', 0.95);
            }
        );
    }, 32);
};

CropComponent.prototype.download = function() {
    var self = this;
    self.downloadButtonNode.disabled = true;

    self.getTransformedImageBlob(function(blob) {
        saveAs(blob, self.getTransformedImageFilename());
        self.downloadButtonNode.disabled = false;
    });
};

CropComponent.prototype.upload = function() {
    var self = this;
    self.uploadButtonNode.disabled = true;

    self.getTransformedImageBlob(function(blob) {
        var form = new FormData();
        form.append('image', blob, self.getTransformedImageFilename());
        self.markers.getPoints().forEach(function(point) {
            form.append('x[]', point.x);
            form.append('y[]', point.y);
        });

        var request = new XMLHttpRequest();
        request.open('POST', self.uploadURL);
        request.addEventListener('load', function() {
            // ...
        });
        request.addEventListener('error', function() {
            alert('Upload error');
        });
        request.addEventListener('abort', function() {
            // ...
        });
        request.addEventListener('loadend', function() {
            self.uploadButtonNode.disabled = false;
        });
        request.send(form);
    });
};
<|endoftext|>


<commit_before>var Tokenizer = function(source) {
    this.line = 1;
    this.source = source || '';
    this.p = 0;
    this.column = 1;
    this.indent = 0;
    this.indent_size = 4;
    this.c = this.source[this.p];
    Lexer.call(this);
}

Tokenizer.prototype = new Lexer();
Tokenizer.prototype.consume = function() {
    if (this.c == '\n' || this.c == '\r') {
        this.column = 0;
    } else {
        this.column++;
    }
    this.p++;
    if (this.p < this.source.length) {
        this.c = this.source[this.p];
    } else {
        this.c = Token.EOF;
    }
}

Tokenizer.prototype.lookahead = function(k) {
    if (this.p + k < this.source.length) {
        return this.source[this.p + k];
    } else {
        return Token.EOF;
    }
}

Tokenizer.prototype.tokenize = function() {

    this.line = 1;
    var tokens = [];

    if (token = this.scanIndent()) {
        tokens.push(token);
    }

    while (this.p < this.source.length) {

        var token;

        if (token = this.scanComment()) {
            tokens.push(token);
            continue;
        }

        if (this.c == '\n' || this.c == '\r') {
            if (token = this.scanLineTerminator()) {
                tokens.push(token);
            }
            if (token = this.scanIndent()) {
                tokens.push(token);
            }
            continue;
        }

        // ignore white spaces
        if (this.isWhiteSpace(this.c)) {
            this.consume();
            continue;
        }

        // ignore semicolon
        if (this.c == ';') {
            this.consume();
            continue;
        }

        if (token = this.scanReservedWord()) {
            tokens.push(token);
            continue;
        }

        if (this.isLetter(this.c)) {
            if (token = this.scanIdent()) {
                tokens.push(token);
                continue;
            }
        }

        if (this.isDigit(this.c)) {
            if (this.isLetter(this.lookahead(1)) && this.lookahead(1) !== 'EOF') {
                var ss = this.c;
                this.consume();
                var ident = this.scanIdent();
                var token = new Token('', ss + ident.text, new Location(this.line, this.column));
                throw new Message(token, Message.IllegalIdent).toString();
            }
            if (token = this.scanDigit()) {
                tokens.push(token);
                continue;
            }
        }

        if (this.c === "'" || this.c == '"') {
            if (token = this.scanString(this.c)) {
                tokens.push(token);
                continue;
            }
        }

        if (token = this.scanPunctuator()) {
            tokens.push(token);
            continue;
        }

        if (token = this.scanRegularExpression()) {
            tokens.push(token);
            continue;
        }

        var token = new Token('', this.c, new Location(this.line, this.column));
        throw new Message(token, Message.UnknownToken).toString();
        this.consume();
    }

    tokens.push(new Token(Token.EOF, '', new Location(this.line, ++this.column)));

    return tokens;
}

Tokenizer.prototype.scanIndent = function() {

    this.indent = 0;

    while (this.p < this.source.length) {
        if (this.c == ' ') this.indent++;
        else if (this.c == '\t') this.indent += this.indent_size;
        else break;
        this.consume();
    }
    return new Token(Token.INDENT, this.indent, new Location(this.line, this.column));
}

Tokenizer.prototype.scanLineTerminator = function() {

    var c = this.c;
    this.line++;
    this.consume();

    if ((c + this.lookahead(1)) == '\r\n') {
        c += this.lookahead(1);
        this.consume();
    }

    // If column is zero, set indent size as column.
    return new Token(Token.NEWLINE, c, new Location(this.line, this.column || (this.indent + 1)));
}

/*
    7.4 Comments

    http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/7_Lexical_Conventions.html#Comment
*/
Tokenizer.prototype.scanComment = function() {

    var sign = this.c;
    if (sign === '#') {
        this.consume();
        var comment = '';
        while (!(this.c === Token.EOF || this.isLineTerminator(this.c))) {
            comment += this.c;
            this.consume();
        }
        return new Token(Token.SINGLE_LINE_COMMENT, comment, new Location(this.line, this.column));
    }

    var sign = this.c + this.lookahead(1);
    if (sign == '/*') {
        this.consume();
        this.consume();
        var comment = '';
        while (this.c + this.lookahead(1) != '*/') {
            if (this.c === Token.EOF) {
                var token = new Token('', this.c, new Location(this.line, this.column || 1));
                throw new Message(token, Message.IllegalComment).toString();
            }
            comment += this.c;
            this.consume();
        }
        this.consume();
        this.consume();
        return new Token(Token.MULTI_LINE_COMMENT, comment, new Location(this.line, this.column));
    }
}

/*
    7.5.1 Reserved Words

    ReservedWord ::
        Keyword
        FutureReservedWord
        NullLiteral
        BooleanLiteral
*/
Tokenizer.prototype.scanReservedWord = function() {

    var token = this.scanKeyword();
    if (token) {
        return token;
    }

    var token = this.scanFutureReservedWord();
    if (token) {
        return token;
    }

    var ident = this.c + this.lookahead(1) + this.lookahead(2) + this.lookahead(3);
    if (ident === 'None') {
        this.consume();
        this.consume();
        this.consume();
        this.consume();
        return new Token(Token.KEYWORDS.NONE, ident, new Location(this.line, this.column));
    }

    var ident = this.c + this.lookahead(1) + this.lookahead(2) + this.lookahead(3);
    if (ident === 'true') {
        this.consume();
        this.consume();
        this.consume();
        this.consume();
        return new Token(Token.BOOLEAN, ident, new Location(this.line, this.column));
    }

    var ident = this.c + this.lookahead(1) + this.lookahead(2) + this.lookahead(3) + this.lookahead(4);
    if (ident === 'false') {
        this.consume();
        this.consume();
        this.consume();
        this.consume();
        this.consume();
        return new Token(Token.BOOLEAN, ident, new Location(this.line, this.column));
    }
}

/*
    7.5.2 Keywords

    Keyword :: one of
        and
        break
        catch
        class
        continue
        delete
        def
        else
        finally
        for
        if
        in
        is
        new
        not
        or
        return
        this
        try
        raise
        void
        while
        xor
*/
Tokenizer.prototype.scanKeyword = function() {

    var keyword = '';
    var p = this.p;
    var c = this.source[this.p];

    while (this.c !== Token.EOF) {
        if (this.isLetter(c) || this.isDigit(c)) {
            keyword += c;
            c = this.source[++p];
            continue;
        }
        break;
    }

    if (Token.KEYWORDS[keyword.toUpperCase()]) {
        var kind = Token.KEYWORDS[keyword.toUpperCase()];
        var i = 0;
        while (i < kind.length) {
            this.consume();
            i++;
        }
        return new Token(kind, keyword, new Location(this.line, this.column));
    }
}

/*
    7.5.3 Future Reserved Words

    FutureReservedWord :: one of
*/
Tokenizer.prototype.scanFutureReservedWord = function() {

}

/*
    7.6 Identifier
*/
Tokenizer.prototype.scanIdent = function() {

    var ident = '';

    while (this.c !== Token.EOF) {
        if (this.isLetter(this.c) || this.isDigit(this.c)) {
            ident += this.c;
            this.consume();
            continue;
        }
        break;
    }

    return new Token(Token.IDENTIFIER, ident, new Location(this.line, this.column));
}

/*
    7.7 Punctuators

    Punctuator :: one of
        { } ( ) [ ] . ; , < > <= >= == != === !==
        + - * % ++ -- << >> >>> & | ^ ! ~ && || ?
        : = += -= *= %= <<= >>= >>>= &= |= ^=

*/
Tokenizer.prototype.scanPunctuator = function() {

    // 1character punctuator

    if (this.c === ':') {
        var c = this.c;
        this.consume();
        return new Token(Token.PUNCTUATOR, c, new Location(this.line, this.column));
    }

    if (this.c === '{' || this.c === '}' || this.c === '(' ||
        this.c === ')' || this.c === '[' || this.c === ']' ||
        this.c === ',' || this.c === '.') {
        var c = this.c;
        this.consume();
        return new Token(Token.PUNCTUATOR, c, new Location(this.line, this.column));
    }

    var token = this.scanAssign();
    if (token) {
        return token;
    }

    var token = this.scanOperator();
    if (token) {
        return token;
    }
}

Tokenizer.prototype.scanOperator = function() {

    // 3character operator
    var op = this.c + this.lookahead(1) + this.lookahead(2);
    if (op === '>>>' || op === '<<<') {
        this.consume();
        this.consume();
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }

    // 2character operator
    var op = this.c + this.lookahead(1);
    if (op === '++' || op === '--' || op === '>>' ||
        op === '<<' || op === '&&' || op === '||') {
        this.consume();
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }

    // 1character operator
    var op = this.c;
    if (op === '+' || op === '-' || op === '*' ||
        op === '/' || op === '%' || op === '<' ||
        op === '>' || op === '&' || op === '!' ||
        op === '|' || op === '^' || op === '~' ||
        op === '?') {
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }
}

Tokenizer.prototype.scanAssign = function() {

    // 4character assignment
    var op = this.c + this.lookahead(1) + this.lookahead(2) + this.lookahead(3);
    if (op === '>>>=') {
        this.consume();
        this.consume();
        this.consume();
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }

    // 3character assignment
    var op = this.c + this.lookahead(1) + this.lookahead(2);
    if (op === '<<=' || op === '>>=' || op === '!==' || op === '===') {
        this.consume();
        this.consume();
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }

    // 2character assignment
    var op = this.c + this.lookahead(1);
    if (op === '*=' || op === '/=' || op === '%=' ||
        op === '+=' || op === '-=' || op === '&=' ||
        op === '^=' || op === '|=' || op === '<=' ||
        op === '>=') {
        this.consume();
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }

    // 1character assignment
    var op = this.c;
    if (op === '=') {
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }
}

/*
    7.8.3 Numeric Literals
*/
Tokenizer.prototype.scanDigit = function() {

    var digit = '';

    while (this.c !== Token.EOF) {
        if (this.isDigit(this.c)) {
            digit += this.c;
            this.consume();
        } else {
            break;
        }
    }

    return new Token(Token.DIGIT, digit, new Location(this.line, this.column));
}

/*
    7.8.4 String Literals
*/
Tokenizer.prototype.scanString = function(delimiter) {

    var ss = '';
    var location = new Location(this.line, this.column);
    this.consume();

    while (1) {
        if (this.c === Token.EOF || this.isLineTerminator(this.c)) {
            var token = new Token('', this.c, new Location(this.line, this.column));
            throw new Message(token, Message.UnexpectedString).toString();
        }
        if (this.c === delimiter) {
            this.consume();
            break;
        }
        ss += this.c;
        this.consume();
    }

    return new Token(Token.STRING, ss, new Location(this.line, this.column));
}

/*
    7.8.5 Regular Expression Literals
*/
Tokenizer.prototype.scanRegularExpression = function() {

}<commit_msg>bug fix about calculating column<commit_after>var Tokenizer = function(source) {
    this.line = 1;
    this.source = source || '';
    this.p = 0;
    this.column = 1;
    this.indent = 0;
    this.indent_size = 4;
    this.c = this.source[this.p];
    Lexer.call(this);
}

Tokenizer.prototype = new Lexer();
Tokenizer.prototype.consume = function() {
    if (this.c == '\n' || this.c == '\r') {
        this.column = 1;
    } else {
        this.column++;
    }
    this.p++;
    if (this.p < this.source.length) {
        this.c = this.source[this.p];
    } else {
        this.c = Token.EOF;
    }
}

Tokenizer.prototype.lookahead = function(k) {
    if (this.p + k < this.source.length) {
        return this.source[this.p + k];
    } else {
        return Token.EOF;
    }
}

Tokenizer.prototype.tokenize = function() {

    this.line = 1;
    var tokens = [];

    if (token = this.scanIndent()) {
        tokens.push(token);
    }

    while (this.p < this.source.length) {

        var token;

        if (token = this.scanComment()) {
            tokens.push(token);
            continue;
        }

        if (this.c == '\n' || this.c == '\r') {
            if (token = this.scanLineTerminator()) {
                tokens.push(token);
            }
            if (token = this.scanIndent()) {
                tokens.push(token);
            }
            continue;
        }

        // ignore white spaces
        if (this.isWhiteSpace(this.c)) {
            this.consume();
            continue;
        }

        // ignore semicolon
        if (this.c == ';') {
            this.consume();
            continue;
        }

        if (token = this.scanReservedWord()) {
            tokens.push(token);
            continue;
        }

        if (this.isLetter(this.c)) {
            if (token = this.scanIdent()) {
                tokens.push(token);
                continue;
            }
        }

        if (this.isDigit(this.c)) {
            if (this.isLetter(this.lookahead(1)) && this.lookahead(1) !== 'EOF') {
                var ss = this.c;
                this.consume();
                var ident = this.scanIdent();
                var token = new Token('', ss + ident.text, new Location(this.line, this.column));
                throw new Message(token, Message.IllegalIdent).toString();
            }
            if (token = this.scanDigit()) {
                tokens.push(token);
                continue;
            }
        }

        if (this.c === "'" || this.c == '"') {
            if (token = this.scanString(this.c)) {
                tokens.push(token);
                continue;
            }
        }

        if (token = this.scanPunctuator()) {
            tokens.push(token);
            continue;
        }

        if (token = this.scanRegularExpression()) {
            tokens.push(token);
            continue;
        }

        var token = new Token('', this.c, new Location(this.line, this.column));
        throw new Message(token, Message.UnknownToken).toString();
        this.consume();
    }

    tokens.push(new Token(Token.EOF, '', new Location(this.line, this.column)));

    return tokens;
}

Tokenizer.prototype.scanIndent = function() {

    this.indent = 0;

    while (this.p < this.source.length) {
        if (this.c == ' ') this.indent++;
        else if (this.c == '\t') this.indent += this.indent_size;
        else break;
        this.consume();
    }
    return new Token(Token.INDENT, this.indent, new Location(this.line, this.column));
}

Tokenizer.prototype.scanLineTerminator = function() {

    var c = this.c;
    this.line++;
    this.consume();

    if ((c + this.lookahead(1)) == '\r\n') {
        c += this.lookahead(1);
        this.consume();
    }

    // If column is zero, set indent size as column.
    return new Token(Token.NEWLINE, c, new Location(this.line, this.column || (this.indent + 1)));
}

/*
    7.4 Comments

    http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/7_Lexical_Conventions.html#Comment
*/
Tokenizer.prototype.scanComment = function() {

    var sign = this.c;
    if (sign === '#') {
        this.consume();
        var comment = '';
        while (!(this.c === Token.EOF || this.isLineTerminator(this.c))) {
            comment += this.c;
            this.consume();
        }
        return new Token(Token.SINGLE_LINE_COMMENT, comment, new Location(this.line, this.column));
    }

    var sign = this.c + this.lookahead(1);
    if (sign == '/*') {
        this.consume();
        this.consume();
        var comment = '';
        while (this.c + this.lookahead(1) != '*/') {
            if (this.c === Token.EOF) {
                var token = new Token('', this.c, new Location(this.line, this.column || 1));
                throw new Message(token, Message.IllegalComment).toString();
            }
            comment += this.c;
            this.consume();
        }
        this.consume();
        this.consume();
        return new Token(Token.MULTI_LINE_COMMENT, comment, new Location(this.line, this.column));
    }
}

/*
    7.5.1 Reserved Words

    ReservedWord ::
        Keyword
        FutureReservedWord
        NullLiteral
        BooleanLiteral
*/
Tokenizer.prototype.scanReservedWord = function() {

    var token = this.scanKeyword();
    if (token) {
        return token;
    }

    var token = this.scanFutureReservedWord();
    if (token) {
        return token;
    }

    var ident = this.c + this.lookahead(1) + this.lookahead(2) + this.lookahead(3);
    if (ident === 'None') {
        this.consume();
        this.consume();
        this.consume();
        this.consume();
        return new Token(Token.KEYWORDS.NONE, ident, new Location(this.line, this.column));
    }

    var ident = this.c + this.lookahead(1) + this.lookahead(2) + this.lookahead(3);
    if (ident === 'true') {
        this.consume();
        this.consume();
        this.consume();
        this.consume();
        return new Token(Token.BOOLEAN, ident, new Location(this.line, this.column));
    }

    var ident = this.c + this.lookahead(1) + this.lookahead(2) + this.lookahead(3) + this.lookahead(4);
    if (ident === 'false') {
        this.consume();
        this.consume();
        this.consume();
        this.consume();
        this.consume();
        return new Token(Token.BOOLEAN, ident, new Location(this.line, this.column));
    }
}

/*
    7.5.2 Keywords

    Keyword :: one of
        and
        break
        catch
        class
        continue
        delete
        def
        else
        finally
        for
        if
        in
        is
        new
        not
        or
        return
        this
        try
        raise
        void
        while
        xor
*/
Tokenizer.prototype.scanKeyword = function() {

    var keyword = '';
    var p = this.p;
    var c = this.source[this.p];

    while (this.c !== Token.EOF) {
        if (this.isLetter(c) || this.isDigit(c)) {
            keyword += c;
            c = this.source[++p];
            continue;
        }
        break;
    }

    if (Token.KEYWORDS[keyword.toUpperCase()]) {
        var text = Token.KEYWORDS[keyword.toUpperCase()];
        var i = 0;
        console.log(this.column)
        while (i < text.length) {
            this.consume();
            i++;
        }
        return new Token(text, keyword, new Location(this.line, this.column));
    }
}

/*
    7.5.3 Future Reserved Words

    FutureReservedWord :: one of
*/
Tokenizer.prototype.scanFutureReservedWord = function() {

}

/*
    7.6 Identifier
*/
Tokenizer.prototype.scanIdent = function() {

    var ident = '';

    while (this.c !== Token.EOF) {
        if (this.isLetter(this.c) || this.isDigit(this.c)) {
            ident += this.c;
            this.consume();
            continue;
        }
        break;
    }

    return new Token(Token.IDENTIFIER, ident, new Location(this.line, this.column));
}

/*
    7.7 Punctuators

    Punctuator :: one of
        { } ( ) [ ] . ; , < > <= >= == != === !==
        + - * % ++ -- << >> >>> & | ^ ! ~ && || ?
        : = += -= *= %= <<= >>= >>>= &= |= ^=

*/
Tokenizer.prototype.scanPunctuator = function() {

    // 1character punctuator

    if (this.c === ':') {
        var c = this.c;
        this.consume();
        return new Token(Token.PUNCTUATOR, c, new Location(this.line, this.column));
    }

    if (this.c === '{' || this.c === '}' || this.c === '(' ||
        this.c === ')' || this.c === '[' || this.c === ']' ||
        this.c === ',' || this.c === '.') {
        var c = this.c;
        this.consume();
        return new Token(Token.PUNCTUATOR, c, new Location(this.line, this.column));
    }

    var token = this.scanAssign();
    if (token) {
        return token;
    }

    var token = this.scanOperator();
    if (token) {
        return token;
    }
}

Tokenizer.prototype.scanOperator = function() {

    // 3character operator
    var op = this.c + this.lookahead(1) + this.lookahead(2);
    if (op === '>>>' || op === '<<<') {
        this.consume();
        this.consume();
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }

    // 2character operator
    var op = this.c + this.lookahead(1);
    if (op === '++' || op === '--' || op === '>>' ||
        op === '<<' || op === '&&' || op === '||') {
        this.consume();
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }

    // 1character operator
    var op = this.c;
    if (op === '+' || op === '-' || op === '*' ||
        op === '/' || op === '%' || op === '<' ||
        op === '>' || op === '&' || op === '!' ||
        op === '|' || op === '^' || op === '~' ||
        op === '?') {
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }
}

Tokenizer.prototype.scanAssign = function() {

    // 4character assignment
    var op = this.c + this.lookahead(1) + this.lookahead(2) + this.lookahead(3);
    if (op === '>>>=') {
        this.consume();
        this.consume();
        this.consume();
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }

    // 3character assignment
    var op = this.c + this.lookahead(1) + this.lookahead(2);
    if (op === '<<=' || op === '>>=' || op === '!==' || op === '===') {
        this.consume();
        this.consume();
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }

    // 2character assignment
    var op = this.c + this.lookahead(1);
    if (op === '*=' || op === '/=' || op === '%=' ||
        op === '+=' || op === '-=' || op === '&=' ||
        op === '^=' || op === '|=' || op === '<=' ||
        op === '>=') {
        this.consume();
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }

    // 1character assignment
    var op = this.c;
    if (op === '=') {
        this.consume();
        return new Token(Token.PUNCTUATOR, op, new Location(this.line, this.column));
    }
}

/*
    7.8.3 Numeric Literals
*/
Tokenizer.prototype.scanDigit = function() {

    var digit = '';

    while (this.c !== Token.EOF) {
        if (this.isDigit(this.c)) {
            digit += this.c;
            this.consume();
        } else {
            break;
        }
    }

    return new Token(Token.DIGIT, digit, new Location(this.line, this.column));
}

/*
    7.8.4 String Literals
*/
Tokenizer.prototype.scanString = function(delimiter) {

    var ss = '';
    var location = new Location(this.line, this.column);
    this.consume();

    while (1) {
        if (this.c === Token.EOF || this.isLineTerminator(this.c)) {
            var token = new Token('', this.c, new Location(this.line, this.column));
            throw new Message(token, Message.UnexpectedString).toString();
        }
        if (this.c === delimiter) {
            this.consume();
            break;
        }
        ss += this.c;
        this.consume();
    }

    return new Token(Token.STRING, ss, new Location(this.line, this.column));
}

/*
    7.8.5 Regular Expression Literals
*/
Tokenizer.prototype.scanRegularExpression = function() {

}<|endoftext|>


<commit_before>/* jslint node: true, esnext: true */

'use strict';

import {
	EOFToken, StringToken, NumberToken, OperatorToken, IdentifierToken, KeywordToken
}
from './known_tokens';

/**
 * @module pratt-parser
 */

export class Tokenizer {

	/**
	 * Creates a tokenizer for later parsing
	 * @param {object} grammar definition of the grammar with operators...
	 */
	constructor(grammar) {
		const maxOperatorLengthForFirstChar = {};
		const registeredTokens = {};

		const operatorTypes = {
			prefix: {
				token: OperatorToken,

				properties: {
					nud: {
						value: function (grammar, left) {
							return this.combine(left, grammar.expression(this.precedence));
						},
						writable: true
					}
				}
			},
			infix: {
				token: OperatorToken,

				properties: {
					led: {
						value: function (grammar, left) {
							return this.combine(left, grammar.expression(this.precedence));
						},
						writable: true
					}
				}
			},
			infixr: {
				token: OperatorToken,

				properties: {
					led: {
						value: function (grammar, left) {
							return this.combine(left, grammar.expression(this.precedence - 1));
						},
						writable: true
					}
				}
			}
		};

		function registerOperator(id, type, options) {

			type.properties.value = {
				value: id
			};

			const op = registeredTokens[id] = Object.assign(Object.create(type.token, type.properties), options);
			return op;
		}

		for (const operatorTypeName in operatorTypes) {
			const ops = grammar[operatorTypeName];
			const operatorType = operatorTypes[operatorTypeName];

			for (const c in ops) {
				const firstChar = c[0];
				const maxLength = maxOperatorLengthForFirstChar[firstChar] || 0;

				if (maxLength < c.length) {
					maxOperatorLengthForFirstChar[firstChar] = c.length;
				}
				registerOperator(c, operatorType, ops[c]);
			}
		}

		Object.defineProperty(this, 'maxOperatorLengthForFirstChar', {
			value: maxOperatorLengthForFirstChar
		});
		Object.defineProperty(this, 'registeredTokens', {
			value: registeredTokens
		});
	}

	/**
	 * @param {string} message
	 * @param {object} context token initiating the error
	 * @param {object} [values]
	 * @return {Object} error
	 */
	error(message, context, values) {
		message = `${context.lineNumber},${context.positionInLine}: ${message}`;
		if (values) message += ': ' + JSON.stringify(values);
		throw new Error(message);
	}

	makeIdentifier(chunk, offset, context, contextProperties) {
		let i = offset;
		i += 1;
		for (;;) {
			const c = chunk[i];
			if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
				(c >= '0' && c <= '9') || c === '_') {
				i += 1;
			} else {
				break;
			}
		}

		contextProperties.value = {
			value: chunk.substring(offset, i)
		};
		return [Object.create(IdentifierToken, contextProperties), i - offset];
	}

	/**
	 * delivers tokens from the input
	 */
	* tokens(chunk, context) {
		let lineNumber = 1;
		let firstCharInLine = 0;
		let i = 0;

		const getContext = () => {
			return {
				lineNumber, positionInLine: i - firstCharInLine
			};
		};

		const getContextProperties = () => {
			return {
				lineNumber: {
					value: lineNumber
				},
				positionInLine: {
					value: i - firstCharInLine
				}
			};
		};

		const length = chunk.length;

		do {
			let c = chunk[i];
			switch (c) {
				case '_':
				case 'A':
				case 'B':
				case 'C':
				case 'D':
				case 'E':
				case 'F':
				case 'G':
				case 'H':
				case 'I':
				case 'J':
				case 'K':
				case 'L':
				case 'M':
				case 'N':
				case 'O':
				case 'P':
				case 'Q':
				case 'R':
				case 'S':
				case 'T':
				case 'U':
				case 'V':
				case 'W':
				case 'X':
				case 'Y':
				case 'Z':
				case 'a':
				case 'b':
				case 'c':
				case 'd':
				case 'e':
				case 'f':
				case 'g':
				case 'h':
				case 'i':
				case 'j':
				case 'k':
				case 'l':
				case 'm':
				case 'n':
				case 'o':
				case 'p':
				case 'q':
				case 'r':
				case 's':
				case 't':
				case 'u':
				case 'v':
				case 'w':
				case 'x':
				case 'y':
				case 'z':
					{
						const [t, l] = this.makeIdentifier(chunk, i, context, getContextProperties());
						i += l;
						yield t;
					}
					break;

				case '0':
				case '1':
				case '2':
				case '3':
				case '4':
				case '5':
				case '6':
				case '7':
				case '8':
				case '9':
					{
						let str = c;
						i += 1;
						for (; i < length;) {
							c = chunk[i];
							if ((c < '0' || c > '9') && c !== '.' && c !== 'e' && c !== 'E') {
								break;
							}
							i += 1;
							str += c;
						}
						yield Object.create(NumberToken, Object.assign(getContextProperties(), {
							value: {
								value: +str
							}
						}));
					}
					break;

				case '"':
				case "'":
					{
						const tc = c;
						let str = '';

						i += 1;
						for (; i < length;) {
							c = chunk[i];
							if (c === tc) {
								i += 1;
								yield Object.create(StringToken, Object.assign(getContextProperties(), {
									value: {
										value: str
									}
								}));
								break;
							} else if (c === '\\') {
								i += 1;
								c = chunk[i];
								switch (c) {
									case 'b':
										c = '\b';
										break;
									case 'f':
										c = '\f';
										break;
									case 'n':
										c = '\n';
										break;
									case 'r':
										c = '\r';
										break;
									case 't':
										c = '\t';
										break;
									case 'u':
										c = parseInt(chunk.substr(i + 1, 4), 16);
										if (!isFinite(c) || c < 0) {
											this.error('Unterminated string', getContext(), {
												value: str
											});
										}
										c = String.fromCharCode(c);
										i += 4;
										break;
								}
								str += c;
								i += 1;
							} else {
								str += c;
								i += 1;
							}
						}
						if (i === length && c !== tc) {
							this.error('Unterminated string', getContext(), {
								value: str
							});
						}
					}
					break;

				case '\n':
					lineNumber += 1;
					firstCharInLine = i;
					i += 1;
					break;

				case '\b':
				case '\f':
				case '\r':
				case '\t':
				case ' ':
					i += 1;
					break;

				case undefined:
					return Object.create(EOFToken, getContextProperties());

				default:
					let t;
					for (let operatorLength = this.maxOperatorLengthForFirstChar[c]; operatorLength > 0; operatorLength--) {
						const c = chunk.substring(i, i + operatorLength);
						t = this.registeredTokens[c];
						if (t) {
							i += operatorLength;
							yield Object.create(t, getContextProperties());
							break;
						}
					}
					if (!t) {
						i += 1;
						this.error('Unknown char', getContext(), {
							value: c
						});
					}
			}
		}
		while (true);
	}
}
<commit_msg>refactor: prepare for better tokenizer<commit_after>/* jslint node: true, esnext: true */

'use strict';

import {
	EOFToken, StringToken, NumberToken, OperatorToken, IdentifierToken, KeywordToken
}
from './known_tokens';

/**
 * @module pratt-parser
 */

export class Tokenizer {

	/**
	 * Creates a tokenizer for later parsing
	 * @param {object} grammar definition of the grammar with operators...
	 */
	constructor(grammar) {
		const maxTokenLengthForFirstChar = {};
		const registeredTokens = {};

		const operatorTypes = {
			prefix: {
				token: OperatorToken,

				properties: {
					nud: {
						value: function (grammar, left) {
							return this.combine(left, grammar.expression(this.precedence));
						},
						writable: true
					}
				}
			},
			infix: {
				token: OperatorToken,

				properties: {
					led: {
						value: function (grammar, left) {
							return this.combine(left, grammar.expression(this.precedence));
						},
						writable: true
					}
				}
			},
			infixr: {
				token: OperatorToken,

				properties: {
					led: {
						value: function (grammar, left) {
							return this.combine(left, grammar.expression(this.precedence - 1));
						},
						writable: true
					}
				}
			}
		};

		function registerOperator(id, type, options) {

			type.properties.value = {
				value: id
			};

			const op = registeredTokens[id] = Object.assign(Object.create(type.token, type.properties), options);
			return op;
		}

		for (const operatorTypeName in operatorTypes) {
			const ops = grammar[operatorTypeName];
			const operatorType = operatorTypes[operatorTypeName];

			for (const c in ops) {
				const firstChar = c[0];
				const maxLength = maxTokenLengthForFirstChar[firstChar] || 0;

				if (maxLength < c.length) {
					maxTokenLengthForFirstChar[firstChar] = c.length;
				}
				registerOperator(c, operatorType, ops[c]);
			}
		}

		Object.defineProperty(this, 'maxTokenLengthForFirstChar', {
			value: maxTokenLengthForFirstChar
		});
		Object.defineProperty(this, 'registeredTokens', {
			value: registeredTokens
		});
	}

	makeIdentifier(chunk, offset, context, contextProperties) {
		let i = offset;
		i += 1;
		for (;;) {
			const c = chunk[i];
			if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
				(c >= '0' && c <= '9') || c === '_') {
				i += 1;
			} else {
				break;
			}
		}

		contextProperties.value = {
			value: chunk.substring(offset, i)
		};
		return [Object.create(IdentifierToken, contextProperties), i - offset];
	}

	/**
	 * delivers tokens from the input
	 */
	* tokens(chunk, context) {
		let lineNumber = 1;
		let firstCharInLine = 0;
		let i = 0;

		const getContext = () => {
			return {
				lineNumber, positionInLine: i - firstCharInLine
			};
		};

		const getContextProperties = () => {
			return {
				lineNumber: {
					value: lineNumber
				},
				positionInLine: {
					value: i - firstCharInLine
				}
			};
		};

		const length = chunk.length;

		do {
			let c = chunk[i];
			switch (c) {
				case '_':
				case 'A':
				case 'B':
				case 'C':
				case 'D':
				case 'E':
				case 'F':
				case 'G':
				case 'H':
				case 'I':
				case 'J':
				case 'K':
				case 'L':
				case 'M':
				case 'N':
				case 'O':
				case 'P':
				case 'Q':
				case 'R':
				case 'S':
				case 'T':
				case 'U':
				case 'V':
				case 'W':
				case 'X':
				case 'Y':
				case 'Z':
				case 'a':
				case 'b':
				case 'c':
				case 'd':
				case 'e':
				case 'f':
				case 'g':
				case 'h':
				case 'i':
				case 'j':
				case 'k':
				case 'l':
				case 'm':
				case 'n':
				case 'o':
				case 'p':
				case 'q':
				case 'r':
				case 's':
				case 't':
				case 'u':
				case 'v':
				case 'w':
				case 'x':
				case 'y':
				case 'z':
					{
						const [t, l] = this.makeIdentifier(chunk, i, context, getContextProperties());
						i += l;
						yield t;
					}
					break;

				case '0':
				case '1':
				case '2':
				case '3':
				case '4':
				case '5':
				case '6':
				case '7':
				case '8':
				case '9':
					{
						let str = c;
						i += 1;
						for (; i < length;) {
							c = chunk[i];
							if ((c < '0' || c > '9') && c !== '.' && c !== 'e' && c !== 'E') {
								break;
							}
							i += 1;
							str += c;
						}
						yield Object.create(NumberToken, Object.assign(getContextProperties(), {
							value: {
								value: +str
							}
						}));
					}
					break;

				case '"':
				case "'":
					{
						const tc = c;
						let str = '';

						i += 1;
						for (; i < length;) {
							c = chunk[i];
							if (c === tc) {
								i += 1;
								yield Object.create(StringToken, Object.assign(getContextProperties(), {
									value: {
										value: str
									}
								}));
								break;
							} else if (c === '\\') {
								i += 1;
								c = chunk[i];
								switch (c) {
									case 'b':
										c = '\b';
										break;
									case 'f':
										c = '\f';
										break;
									case 'n':
										c = '\n';
										break;
									case 'r':
										c = '\r';
										break;
									case 't':
										c = '\t';
										break;
									case 'u':
										c = parseInt(chunk.substr(i + 1, 4), 16);
										if (!isFinite(c) || c < 0) {
											this.error('Unterminated string', getContext(), {
												value: str
											});
										}
										c = String.fromCharCode(c);
										i += 4;
										break;
								}
								str += c;
								i += 1;
							} else {
								str += c;
								i += 1;
							}
						}
						if (i === length && c !== tc) {
							this.error('Unterminated string', getContext(), {
								value: str
							});
						}
					}
					break;

				case '\n':
					lineNumber += 1;
					firstCharInLine = i;
					i += 1;
					break;

				case '\b':
				case '\f':
				case '\r':
				case '\t':
				case ' ':
					i += 1;
					break;

				case undefined:
					return Object.create(EOFToken, getContextProperties());

				default:
					let t;
					for (let operatorLength = this.maxTokenLengthForFirstChar[c]; operatorLength > 0; operatorLength--) {
						const c = chunk.substring(i, i + operatorLength);
						t = this.registeredTokens[c];
						if (t) {
							i += operatorLength;
							yield Object.create(t, getContextProperties());
							break;
						}
					}
					if (!t) {
						i += 1;
						this.error('Unknown char', getContext(), {
							value: c
						});
					}
			}
		}
		while (true);
	}

	/**
	 * @param {string} message
	 * @param {object} context token initiating the error
	 * @param {object} [values]
	 * @return {Object} error
	 */
	error(message, context, values) {
		message = `${context.lineNumber},${context.positionInLine}: ${message}`;
		if (values) message += ': ' + JSON.stringify(values);
		throw new Error(message);
	}
}
<|endoftext|>


<commit_before>define(['logger', 'tracker', 'underscore', 'jquery', 'backbone', 'spin', 'voc', 'userParams',
        'utils/InputValidation',
        'text!templates/toolbar/activity_stream.tpl', 'text!templates/toolbar/components/selected_user.tpl',
        'view/sss/MessageView', 'view/sss/ActivityView', 'view/sss/EntityRecommendationView',
        'data/sss/MessageData', 'data/sss/ActivityData', 'data/EntityData',
        'utils/SearchHelper'], function(Logger, tracker, _, $, Backbone, Spinner, Voc, userParams, InputValidation, ActivityStreamTemplate, SelectedUserTemplate, MessageView, ActivityView, EntityRecommendationView, MessageData, ActivityData, EntityData, SearchHelper){
    return Backbone.View.extend({
        events: {
            'keypress textarea[name="messageText"]' : 'updateOnEnter',
            'keyup textarea[name="messageText"]' : 'revalidateMessageText',
            'click .selectedUser > span' : 'removeSelectedUser',
            'change input[name="showInToolbar[]"]' : 'filterStream',
            'bnp:markMessageAsRead' : 'reduceAndUpdateUnreadMessagesCount'
        },
        LOG: Logger.get('ActivityStreamToolbarView'),
        initialize: function() {
            this.messageTextCharactersLimit = 300;
            this.activityResultViews = [];
            this.messageResultViews = [];
            this.recommendationsResultViews = [];
            this.selectedUsers = [];
            this.unreadMessagesCount = 0;
            this.messageRecipientSelector = 'input[name="messageRecipient"]';
            this.messageTextSelector = 'textarea[name="messageText"]';

            this.fetchActivityStream();
        },
        render: function() {
            var that = this,
                notifications = _.template(ActivityStreamTemplate);
            this.$el.html(notifications);

            // Initialize user select autocomplete
            this.$el.find(this.messageRecipientSelector).autocomplete({
                source: SearchHelper.userAutocompleteSource,
                select: function(event, ui) {
                    event.preventDefault();
                    that.addSelectedUser(event, ui, this);
                }
            });

            this.resetCharactersRemaining();
            this.$el.find(this.messageTextSelector).on('keyup', function(e) {
                var thisElem = $(this),
                    charsRemainingElem = thisElem.next().find('.charactersRemaining'),
                    remaining = that.messageTextCharactersLimit - thisElem.val().length;

                if ( remaining === 0 ) {
                    e.preventDefault();
                } else if ( remaining < 0 ) {
                    thisElem.val(thisElem.val().substring(0, that.messageTextCharactersLimit));
                }

                charsRemainingElem.html( ( remaining >= 0 ) ? remaining : 0 );
            });
        },
        updateOnEnter: function(e) {
            if (e.keyCode == 13) {
                this.LOG.debug('updateOnEnter', e);
                e.preventDefault();
                this.sendMessage(e);
            }
        },
        addSelectedUser: function(event, ui, autocomplete) {
            if ( _.indexOf(this.selectedUsers, ui.item.value) === -1 ) {
                var input = $(autocomplete);
                this.selectedUsers.push(ui.item.value);
                input.val('');
                input.parent().append(_.template(SelectedUserTemplate, {
                    value : ui.item.value,
                    label : ui.item.label
                }));
                this.validateMessageRecipient();
                // Disable input as only one recipient is allowed
                input.prop('disabled', true);
            }
        },
        removeSelectedUser: function(e) {
            var currentTarget = $(e.currentTarget),
                removable = currentTarget.parent();
            this.selectedUsers = _.without(this.selectedUsers, removable.data('value'));

            removable.remove();
            this.validateMessageRecipient();
            // Enable input once recipient is removed
            this.$el.find(this.messageRecipientSelector).prop('disabled', false);
        },
        addAjaxLoader: function(element) {
            if ( !this.spinner ) {
                this.spinner = new Spinner({
                    radius : 5,
                    length : 5,
                    width : 2
                });
            }
            var wrapper = document.createElement('div');
            wrapper.className = 'ajaxLoader';
            element.prepend(wrapper);
            this.spinner.spin(wrapper);
        },
        removeAjaxLoader: function(element) {
            this.spinner.stop();
            element.find('.ajaxLoader').remove();
        },
        sendMessage: function(e) {
            var currentTarget = $(e.currentTarget);
            if ( this.messageBeingSent === true ) {
                return false;
            }

            var that = this,
                hasErrors = false;

            if ( !this.validateMessageRecipient() ) {
                hasErrors = true;
            }

            if ( !this.validateMessageText() ) {
                hasErrors = true;
            }

            if ( hasErrors ) {
                return false;
            }

            this.messageBeingSent = true;
            InputValidation.removeAlerts(this.$el.find('.writeMessage'));
            this.addAjaxLoader(currentTarget.parent());

            var promise = MessageData.sendMessage(this.selectedUsers[0], this.$el.find(this.messageTextSelector).val());

            promise.done(function() {
                that.removeAjaxLoader(currentTarget.parent());
                that.messageBeingSent = false;
                that._cleanUpAdterSendMessage();
            });

            promise.fail(function() {
                that.removeAjaxLoader(currentTarget.parent());
                that.messageBeingSent = false;
                InputValidation.addAlert(that.$el.find('.writeMessage > label'), 'alert-danger', 'Message could not be sent! Please try again.');
            });
        },
        _cleanUpAdterSendMessage: function() {
            var that = this;

            this.$el.find(this.messageRecipientSelector).val('');
            this.$el.find('.selectedUser').remove();
            this.selectedUsers = [];
            // Enable user selector once cleanUp procedure runs
            this.$el.find(this.messageRecipientSelector).prop('disabled', false);

            // Disable message text input validation temporarily
            this.disableMessageTextValidation = true;
            this.$el.find(this.messageTextSelector).val('');

            this.resetCharactersRemaining();
        },
        validateMessageRecipient: function() {
            var element = this.$el.find(this.messageRecipientSelector),
                alertText = 'Please select at least one user from suggested list!';

            return InputValidation.validateUserSelect(element, this.selectedUsers, alertText);
        },
        validateMessageText: function() {
            var element = this.$el.find(this.messageTextSelector),
                alertText = 'Please provide message text!';

            return InputValidation.validateTextInput(element, alertText);
        },
        revalidateMessageText: function(e) {
            if ( !this.disableMessageTextValidation ) {
                this.validateMessageText();
            } else {
                // Re-enable message text input validation
                this.disableMessageTextValidation = false;
            }
        },
        fetchActivities: function() {
            var data = {
                types : [
                    'addCategory',
                    'removeCategories',
                    //'createLearnEp',
                    'addEntityToLearnEpVersion',
                    'changeEntityForLearnEpVersionEntity',
                    'moveLearnEpVersionEntity',
                    'removeLearnEpVersionEntity',
                    'addCircleToLearnEpVersion',
                    'changeLearnEpVersionCircleLabel',
                    'moveLearnEpVersionCircle',
                    'removeLearnEpVersionCircle',
                    'shareLearnEpWithUser'
                ]
            },
            promise = ActivityData.getActivities(data);

            return promise;
        },
        fetchMessages: function() {
            var promise = MessageData.getMessages(true);

            return promise;
        },
        fetchRecommendations: function() {
            var data = {
                    forUser : userParams.user,
                    maxResources : 20,
                    typesToRecommOnly : ['evernoteResource', 'evernoteNote', 'evernoteNotebook']
                },
                promise = EntityData.getRecommResources(data);

            return promise;
        },
        fetchActivityStream: function() {
            var that = this,
                activitiesPromise = this.fetchActivities(),
                messagesPromise = this.fetchMessages(),
                recommendationsPromise = this.fetchRecommendations();

            // XXX Even if one of the call is rejected, nothing will be displayed
            // Probably need to tune the corresponding calls to always resolve
            // just resolve with an empty array in case of failure.
            $.when(activitiesPromise, messagesPromise, recommendationsPromise)
                .done(function(activities, messages, recommendations) {
                    that.LOG.debug('fetchActivityStream', activities, messages, recommendations);

                    // Deal with activities
                    if ( !_.isEmpty(that.activityResultViews) ) {
                        _.each(that.activityResultViews, function(view) {
                            view.remove();
                        });
                        that.activityResultViews = [];
                    }
                    _.each(activities, function(activity) {
                        var view = new ActivityView({
                            model : activity
                        });
                        that.activityResultViews.push(view);
                    });

                    // Deal with messages
                    if ( !_.isEmpty(that.messageResultViews) ) {
                        _.each(that.messageResultViews, function(view) {
                            view.remove();
                        });
                        that.messageResultViews = [];
                        that.unreadMessagesCount = 0;
                    }
                    _.each(messages, function(message) {
                        var view = new MessageView({
                            model : message
                        });
                        that.messageResultViews.push(view);
                        if ( view.model.get(Voc.isRead) !== true ) {
                            that.unreadMessagesCount += 1;
                        }
                    });
                    that.addUpdateUnreadMessagesCount();

                    // Deal with recommendations
                    if ( !_.isEmpty(that.recommendationsResultViews) ) {
                        _.each(that.recommendationsResultViews, function(view) {
                            view.remove();
                        });
                        that.recommendationsResultViews = [];
                    }
                    _.each(recommendations, function(recommendation) {
                        var view = new EntityRecommendationView({
                            model : recommendation
                        });
                        that.recommendationsResultViews.push(view);
                    });

                    that.displayActivityStream();
                });
            // TODO Check if fail handler is also needed
        },
        displayActivityStream: function() {
            var resultSet = this.$el.find('.stream .resultSet');
                combined = this.activityResultViews.concat(this.messageResultViews, this.recommendationsResultViews);

            var sortedViews = _.sortBy(combined, function(view) {
                // Get reverse sort order
                return view.model.get(Voc.creationTime) * -1;
            });

            _.each(sortedViews, function(view) {
                resultSet.append(view.render().$el);
            });
        },
        _showHideStreamViews: function(views, doShow) {
           if ( views.length > 0 ) {
               _.each(views, function(view) {
                   if ( doShow ) {
                       view.$el.show();
                   } else {
                       view.$el.hide();
                   }
               });
           }
        },
        filterStream: function(e) {
            this.LOG.debug('FilterStream', e);
            var currentTarget = $(e.currentTarget),
                value = currentTarget.val(),
                isChecked = currentTarget.is(':checked'),
                views = [];

            this.$el.find('input[name="showInToolbar[]"]').prop('disabled', true);

            switch (value) {
                case 'activities':
                    views = this.activityResultViews;
                    break;
                case 'messages':
                    views = this.messageResultViews;
                    break;
                case 'notifications':
                    // TODO Handle this when implemented
                    break;
                case 'recommendations':
                    views = this.recommendationsResultViews;
                    break;
            }

            this._showHideStreamViews(views, isChecked);
            this.$el.find('input[name="showInToolbar[]"]').prop('disabled', false);
        },
        addUpdateUnreadMessagesCount: function() {
            var showMessagesLabel = this.$el.find('label[for="showMessages"]'),
                theCountBadge = showMessagesLabel.find('span.badge');

            if ( this.unreadMessagesCount > 0) {
                if ( theCountBadge.length > 0 ) {
                    theCountBadge.html(this.unreadMessagesCount);
                } else {
                    showMessagesLabel.append(' ' + this.unreadMessagesCount + '');
                }
            } else {
                if ( theCountBadge.length > 0 ) {
                    theCountBadge.remove();
                }
            }
        },
        reduceAndUpdateUnreadMessagesCount: function(e) {
            this.unreadMessagesCount -= 1;
            this.addUpdateUnreadMessagesCount();
        },
        resetCharactersRemaining: function() {
            this.$el.find(this.messageTextSelector).next().find('.charactersRemaining').html(this.messageTextCharactersLimit);
        }
    });
});
<commit_msg>LL-1006 #close Added parameter to the call on the BnP side.<commit_after>define(['logger', 'tracker', 'underscore', 'jquery', 'backbone', 'spin', 'voc', 'userParams',
        'utils/InputValidation',
        'text!templates/toolbar/activity_stream.tpl', 'text!templates/toolbar/components/selected_user.tpl',
        'view/sss/MessageView', 'view/sss/ActivityView', 'view/sss/EntityRecommendationView',
        'data/sss/MessageData', 'data/sss/ActivityData', 'data/EntityData',
        'utils/SearchHelper'], function(Logger, tracker, _, $, Backbone, Spinner, Voc, userParams, InputValidation, ActivityStreamTemplate, SelectedUserTemplate, MessageView, ActivityView, EntityRecommendationView, MessageData, ActivityData, EntityData, SearchHelper){
    return Backbone.View.extend({
        events: {
            'keypress textarea[name="messageText"]' : 'updateOnEnter',
            'keyup textarea[name="messageText"]' : 'revalidateMessageText',
            'click .selectedUser > span' : 'removeSelectedUser',
            'change input[name="showInToolbar[]"]' : 'filterStream',
            'bnp:markMessageAsRead' : 'reduceAndUpdateUnreadMessagesCount'
        },
        LOG: Logger.get('ActivityStreamToolbarView'),
        initialize: function() {
            this.messageTextCharactersLimit = 300;
            this.activityResultViews = [];
            this.messageResultViews = [];
            this.recommendationsResultViews = [];
            this.selectedUsers = [];
            this.unreadMessagesCount = 0;
            this.messageRecipientSelector = 'input[name="messageRecipient"]';
            this.messageTextSelector = 'textarea[name="messageText"]';

            this.fetchActivityStream();
        },
        render: function() {
            var that = this,
                notifications = _.template(ActivityStreamTemplate);
            this.$el.html(notifications);

            // Initialize user select autocomplete
            this.$el.find(this.messageRecipientSelector).autocomplete({
                source: SearchHelper.userAutocompleteSource,
                select: function(event, ui) {
                    event.preventDefault();
                    that.addSelectedUser(event, ui, this);
                }
            });

            this.resetCharactersRemaining();
            this.$el.find(this.messageTextSelector).on('keyup', function(e) {
                var thisElem = $(this),
                    charsRemainingElem = thisElem.next().find('.charactersRemaining'),
                    remaining = that.messageTextCharactersLimit - thisElem.val().length;

                if ( remaining === 0 ) {
                    e.preventDefault();
                } else if ( remaining < 0 ) {
                    thisElem.val(thisElem.val().substring(0, that.messageTextCharactersLimit));
                }

                charsRemainingElem.html( ( remaining >= 0 ) ? remaining : 0 );
            });
        },
        updateOnEnter: function(e) {
            if (e.keyCode == 13) {
                this.LOG.debug('updateOnEnter', e);
                e.preventDefault();
                this.sendMessage(e);
            }
        },
        addSelectedUser: function(event, ui, autocomplete) {
            if ( _.indexOf(this.selectedUsers, ui.item.value) === -1 ) {
                var input = $(autocomplete);
                this.selectedUsers.push(ui.item.value);
                input.val('');
                input.parent().append(_.template(SelectedUserTemplate, {
                    value : ui.item.value,
                    label : ui.item.label
                }));
                this.validateMessageRecipient();
                // Disable input as only one recipient is allowed
                input.prop('disabled', true);
            }
        },
        removeSelectedUser: function(e) {
            var currentTarget = $(e.currentTarget),
                removable = currentTarget.parent();
            this.selectedUsers = _.without(this.selectedUsers, removable.data('value'));

            removable.remove();
            this.validateMessageRecipient();
            // Enable input once recipient is removed
            this.$el.find(this.messageRecipientSelector).prop('disabled', false);
        },
        addAjaxLoader: function(element) {
            if ( !this.spinner ) {
                this.spinner = new Spinner({
                    radius : 5,
                    length : 5,
                    width : 2
                });
            }
            var wrapper = document.createElement('div');
            wrapper.className = 'ajaxLoader';
            element.prepend(wrapper);
            this.spinner.spin(wrapper);
        },
        removeAjaxLoader: function(element) {
            this.spinner.stop();
            element.find('.ajaxLoader').remove();
        },
        sendMessage: function(e) {
            var currentTarget = $(e.currentTarget);
            if ( this.messageBeingSent === true ) {
                return false;
            }

            var that = this,
                hasErrors = false;

            if ( !this.validateMessageRecipient() ) {
                hasErrors = true;
            }

            if ( !this.validateMessageText() ) {
                hasErrors = true;
            }

            if ( hasErrors ) {
                return false;
            }

            this.messageBeingSent = true;
            InputValidation.removeAlerts(this.$el.find('.writeMessage'));
            this.addAjaxLoader(currentTarget.parent());

            var promise = MessageData.sendMessage(this.selectedUsers[0], this.$el.find(this.messageTextSelector).val());

            promise.done(function() {
                that.removeAjaxLoader(currentTarget.parent());
                that.messageBeingSent = false;
                that._cleanUpAdterSendMessage();
            });

            promise.fail(function() {
                that.removeAjaxLoader(currentTarget.parent());
                that.messageBeingSent = false;
                InputValidation.addAlert(that.$el.find('.writeMessage > label'), 'alert-danger', 'Message could not be sent! Please try again.');
            });
        },
        _cleanUpAdterSendMessage: function() {
            var that = this;

            this.$el.find(this.messageRecipientSelector).val('');
            this.$el.find('.selectedUser').remove();
            this.selectedUsers = [];
            // Enable user selector once cleanUp procedure runs
            this.$el.find(this.messageRecipientSelector).prop('disabled', false);

            // Disable message text input validation temporarily
            this.disableMessageTextValidation = true;
            this.$el.find(this.messageTextSelector).val('');

            this.resetCharactersRemaining();
        },
        validateMessageRecipient: function() {
            var element = this.$el.find(this.messageRecipientSelector),
                alertText = 'Please select at least one user from suggested list!';

            return InputValidation.validateUserSelect(element, this.selectedUsers, alertText);
        },
        validateMessageText: function() {
            var element = this.$el.find(this.messageTextSelector),
                alertText = 'Please provide message text!';

            return InputValidation.validateTextInput(element, alertText);
        },
        revalidateMessageText: function(e) {
            if ( !this.disableMessageTextValidation ) {
                this.validateMessageText();
            } else {
                // Re-enable message text input validation
                this.disableMessageTextValidation = false;
            }
        },
        fetchActivities: function() {
            var data = {
                types : [
                    'addCategory',
                    'removeCategories',
                    //'createLearnEp',
                    'addEntityToLearnEpVersion',
                    'changeEntityForLearnEpVersionEntity',
                    'moveLearnEpVersionEntity',
                    'removeLearnEpVersionEntity',
                    'addCircleToLearnEpVersion',
                    'changeLearnEpVersionCircleLabel',
                    'moveLearnEpVersionCircle',
                    'removeLearnEpVersionCircle',
                    'shareLearnEpWithUser'
                ],
                includeOnlyLastActivities : true
            },
            promise = ActivityData.getActivities(data);

            return promise;
        },
        fetchMessages: function() {
            var promise = MessageData.getMessages(true);

            return promise;
        },
        fetchRecommendations: function() {
            var data = {
                    forUser : userParams.user,
                    maxResources : 20,
                    typesToRecommOnly : ['evernoteResource', 'evernoteNote', 'evernoteNotebook']
                },
                promise = EntityData.getRecommResources(data);

            return promise;
        },
        fetchActivityStream: function() {
            var that = this,
                activitiesPromise = this.fetchActivities(),
                messagesPromise = this.fetchMessages(),
                recommendationsPromise = this.fetchRecommendations();

            // XXX Even if one of the call is rejected, nothing will be displayed
            // Probably need to tune the corresponding calls to always resolve
            // just resolve with an empty array in case of failure.
            $.when(activitiesPromise, messagesPromise, recommendationsPromise)
                .done(function(activities, messages, recommendations) {
                    that.LOG.debug('fetchActivityStream', activities, messages, recommendations);

                    // Deal with activities
                    if ( !_.isEmpty(that.activityResultViews) ) {
                        _.each(that.activityResultViews, function(view) {
                            view.remove();
                        });
                        that.activityResultViews = [];
                    }
                    _.each(activities, function(activity) {
                        var view = new ActivityView({
                            model : activity
                        });
                        that.activityResultViews.push(view);
                    });

                    // Deal with messages
                    if ( !_.isEmpty(that.messageResultViews) ) {
                        _.each(that.messageResultViews, function(view) {
                            view.remove();
                        });
                        that.messageResultViews = [];
                        that.unreadMessagesCount = 0;
                    }
                    _.each(messages, function(message) {
                        var view = new MessageView({
                            model : message
                        });
                        that.messageResultViews.push(view);
                        if ( view.model.get(Voc.isRead) !== true ) {
                            that.unreadMessagesCount += 1;
                        }
                    });
                    that.addUpdateUnreadMessagesCount();

                    // Deal with recommendations
                    if ( !_.isEmpty(that.recommendationsResultViews) ) {
                        _.each(that.recommendationsResultViews, function(view) {
                            view.remove();
                        });
                        that.recommendationsResultViews = [];
                    }
                    _.each(recommendations, function(recommendation) {
                        var view = new EntityRecommendationView({
                            model : recommendation
                        });
                        that.recommendationsResultViews.push(view);
                    });

                    that.displayActivityStream();
                });
            // TODO Check if fail handler is also needed
        },
        displayActivityStream: function() {
            var resultSet = this.$el.find('.stream .resultSet');
                combined = this.activityResultViews.concat(this.messageResultViews, this.recommendationsResultViews);

            var sortedViews = _.sortBy(combined, function(view) {
                // Get reverse sort order
                return view.model.get(Voc.creationTime) * -1;
            });

            _.each(sortedViews, function(view) {
                resultSet.append(view.render().$el);
            });
        },
        _showHideStreamViews: function(views, doShow) {
           if ( views.length > 0 ) {
               _.each(views, function(view) {
                   if ( doShow ) {
                       view.$el.show();
                   } else {
                       view.$el.hide();
                   }
               });
           }
        },
        filterStream: function(e) {
            this.LOG.debug('FilterStream', e);
            var currentTarget = $(e.currentTarget),
                value = currentTarget.val(),
                isChecked = currentTarget.is(':checked'),
                views = [];

            this.$el.find('input[name="showInToolbar[]"]').prop('disabled', true);

            switch (value) {
                case 'activities':
                    views = this.activityResultViews;
                    break;
                case 'messages':
                    views = this.messageResultViews;
                    break;
                case 'notifications':
                    // TODO Handle this when implemented
                    break;
                case 'recommendations':
                    views = this.recommendationsResultViews;
                    break;
            }

            this._showHideStreamViews(views, isChecked);
            this.$el.find('input[name="showInToolbar[]"]').prop('disabled', false);
        },
        addUpdateUnreadMessagesCount: function() {
            var showMessagesLabel = this.$el.find('label[for="showMessages"]'),
                theCountBadge = showMessagesLabel.find('span.badge');

            if ( this.unreadMessagesCount > 0) {
                if ( theCountBadge.length > 0 ) {
                    theCountBadge.html(this.unreadMessagesCount);
                } else {
                    showMessagesLabel.append(' ' + this.unreadMessagesCount + '');
                }
            } else {
                if ( theCountBadge.length > 0 ) {
                    theCountBadge.remove();
                }
            }
        },
        reduceAndUpdateUnreadMessagesCount: function(e) {
            this.unreadMessagesCount -= 1;
            this.addUpdateUnreadMessagesCount();
        },
        resetCharactersRemaining: function() {
            this.$el.find(this.messageTextSelector).next().find('.charactersRemaining').html(this.messageTextCharactersLimit);
        }
    });
});
<|endoftext|>