<commit_before>'use strict';
const parseRule = require('./parseRule');
const converter = require('./unitConverter');
const SG = require('ml-savitzky-golay');
const GpxHelper = require('./GpxHelper');

const SGDefault = {
    windowSize: 31,
    pad: 'pre',
    derivative: 0
};

class Route {
    constructor(segments, options) {
        options = options || {};
        // Make a deep copy
        this.segments = segments.map(segment => {
            return {
                distance: segment.distance,
                duration: segment.duration,
                elevation: segment.elevation
            };
        });
        this._timeUnit = options.timeUnit || 'h';
        this._lengthUnit = options.distanceUnit || 'km';
        this._speedUnit = this._lengthUnit + '/' + this._timeUnit;
        this._paceUnit = this._timeUnit + '/' + this._lengthUnit;
        if(options.SG) {
            this._SG = Object.assign({}, SGDefault, options.SG);
        }
    }

    get distance() {
        return this.segments.map(segment => segment.distance);
    }

    get duration() {
        return this.segments.map(segment => segment.duration);
    }

    get elevation() {
        return this.segments.map(segment => segment.elevation);
    }

    _getComputedProperty(property) {
        var prop = '_' + property;
        if(!this[prop]) {
            this._compute();
        }
        return this[prop];
    }

    get speed() {
        return this._getComputedProperty('speed');
    }

    get elevationSpeed() {
        return this._getComputedProperty('elevationSpeed');
    }

    get meanSpeed() {
        return this._getComputedProperty('meanSpeed');
    }

    get meanPace() {
        return this._getComputedProperty('meanPace');
    }

    get totalDuration() {
        return this._getComputedProperty('totalDuration');
    }

    get totalDistance() {
        return this._getComputedProperty('totalDistance');
    }

    get totalElevation() {
        return this._getComputedProperty('totalElevation');
    }

    get totalUp() {
        return this._getComputedProperty('totalUp');
    }

    get totalDown() {
        return this._getComputedProperty('totalDown');
    }

    get cumulDuration() {
        return this._getComputedProperty('cumulDuration');
    }

    get cumulDistance() {
        return this._getComputedProperty('cumulDistance');
    }

    get cumulElevation() {
        return this._getComputedProperty('cumulElevation');
    }

    split(splitRule) {
        var that = this;
        var distances = this.cumulDistance;
        var times = this.cumulDuration;
        var elevations = this.cumulElevation;
        var remainderD = 0;
        var remainderT = 0;
        var remainderE = 0;

        function getNextIdx(type, property, idx, val) {
            var measures, remainder;
            if (type === 'joker' && val === '*') return distances.length - 1;
            if (property === 'cumulDistance') {
                val = converter.convert(val, that._lengthUnit);
                measures = distances;
                remainder = remainderD;
            } else if (property === 'cumulDuration') {
                val = converter.convert(val, that._timeUnit);
                measures = times;
                remainder = remainderT;
            } else if (property === 'cumulElevation') {
                val = converter.convert(val, that._lengthUnit);
                measures = elevations;
                remainder = remainderE;
            }
            var i = idx;
            var comp = val - remainder;

            while (i < measures.length - 1) {
                if (comp <= measures[i] - measures[idx]) {
                    var f = (measures[i] - measures[i - 1]) / (measures[i] - measures[idx] - val + remainder);
                    if (property === 'cumulDistance') {
                        remainderD = distances[i] - distances[idx] - val + remainder;
                        remainderT = (times[i] - times[i - 1]) / f;
                        remainderE = (elevations[i] - elevations[i - 1]) / f;
                    } else if (property === 'cumulDuration') {
                        remainderT = times[i] - times[idx] - val + remainder;
                        remainderD = (distances[i] - distances[i - 1]) / f;
                        remainderE = (elevations[i] - elevations[i - 1]) / f;
                    } else {
                        remainderE = elevations[i] - elevations[idx] - val + remainder;
                        remainderT = (times[i] - times[i - 1]) / f;
                        remainderD = (distances[i] - distances[i - 1]) / f;
                    }
                    break;
                }
                i++;
            }
            return i;
        }

        function getNextAutoIdx(route, thresholds, from) {
            var to = route.segments.length - 1;
            var property = thresholds[0].property;
            var unitProperty = '_' + thresholds[0].type + 'Unit';
            // convert thresholds to matching units
            for (var i = 0; i < thresholds.length; i++) {
                thresholds[i].value = converter.convert(thresholds[i].value, route[unitProperty]);
            }
            var condition = getAutoCondition(thresholds.map(t => t.value), route[property][from]);
            ++from;
            while (condition(route[property][from]) && from <= to) {
                ++from;
            }
            return from;
        }

        var serie = parseRule(splitRule);
        var parts = [];

        var idx = 0, from;
        for (var i = 0; i < serie.length; i++) {
            from = idx;
            idx = getNextIdx(serie[i].type, serie[i].property, idx, serie[i].value);
            parts.push({from, to: idx});
        }

        var split = new Array(parts.length);
        for (var i = 0; i < parts.length; i++) {
            split[i] = Route.fromSegments(this.segments.slice(parts[i].from, parts[i].to), {
                timeUnit: that._timeUnit,
                distanceUnit: that._lengthUnit
            });

            if (serie[i].thresholds) {
                var fromTo = [];
                from = 0;
                while (from < split[i].cumulDistance.length - 1) {
                    var thresholds = serie[i].thresholds.slice().map(obj => Object.assign({}, obj));
                    var f = from;
                    from = getNextAutoIdx(split[i], thresholds, from);
                    fromTo.push({from: f, to: from});
                }
                var subsplit = new Array(fromTo.length);
                for (var j = 0; j < fromTo.length; j++) {
                    subsplit[j] = Route.fromSegments(split[i].segments.slice(fromTo[j].from, fromTo[j].to));
                }
                split[i] = subsplit;
            }
        }

        return flatten(split);
    }

    setUnits(distanceUnit, timeUnit) {
        var hasChanged = false, distanceUnitChanged = false, timeUnitChanged = false;
        if (distanceUnit && distanceUnit !== this._lengthUnit) {
            hasChanged = true;
            distanceUnitChanged = true;
        }
        if (timeUnit && this._timeUnit !== timeUnit) {
            hasChanged = true;
            timeUnitChanged = true;
        }
        if (!hasChanged) return;
        if (distanceUnitChanged) {
            for (var i = 0; i < this.segments.length; i++) {
                this.segments[i].distance = converter.convert(this.segments[i].distance + this._lengthUnit, distanceUnit);
                this.segments[i].elevation = converter.convert(this.segments[i].elevation + this._lengthUnit, distanceUnit);
            }
            this._lengthUnit = distanceUnit;
        }
        if (timeUnitChanged) {
            for (i = 0; i < this.segments.length; i++) {
                this.segments[i].duration = converter.convert(this.segments[i].duration + this._timeUnit, timeUnit);
            }
            this._timeUnit = timeUnit;
        }

        this._speedUnit = this._lengthUnit + '/' + this._timeUnit;
        this._paceUnit = this._timeUnit + '/' + this._lengthUnit;
        if (hasChanged && this._computeCalled) {
            this._compute();
        }

    }

    _compute() {
        this._computeCalled = true;
        this._cumulDistance = new Array(this.segments.length);
        this._cumulDuration = new Array(this.segments.length);
        this._cumulElevation = new Array(this.segments.length);
        this._totalDistance = 0;
        this._totalDuration = 0;
        this._totalElevation = 0;
        this._totalUp = 0;
        this._totalDown = 0;
        this._meanSpeed = 0;
        this._speed = new Array(this.segments.length);
        this._elevationSpeed = new Array(this.segments.length);
        var cumSpeed = 0;

        for (var i = 0; i < this.segments.length; i++) {
            var segment = this.segments[i];
            this._speed[i] = segment.distance / segment.duration;
            this._elevationSpeed[i] = segment.elevation / segment.duration;
            this._totalDuration += segment.duration;
            this._totalDistance += segment.distance;
            this._totalElevation += segment.elevation;
            if(segment.elevation > 0) {
                this._totalUp += segment.elevation;
            } else {
                this._totalDown += segment.elevation;
            }
            this._cumulDuration [i] = this._totalDuration;
            this._cumulDistance[i] = this._totalDistance;
            this._cumulElevation[i] = this._totalElevation;
            cumSpeed += this._speed[i];
        }

        this._meanSpeed = cumSpeed / this.segments.length;
        this._meanPace = 1 / this._meanSpeed;
        this._cumulDistance.unshift(0);
        this._cumulDuration.unshift(0);
        this._cumulElevation.unshift(0);
        if(this._SG) {
            this._speed = SG(this._speed, 1, this._SG);
            this._elevationSpeed = SG(this._elevationSpeed, 1, this._SG);
        }
    }

    static fromGpx(gpx, options) {
        var gpxHelper = new GpxHelper(gpx);
        return gpxHelper.compute(0,0).then(segments => {
            return Route.fromSegments(segments, options);
        });
    }

    static fromSegments(segments, options) {
        return new Route(segments, options);
    }

    static fromRule(rule) {
        throw new Error('not yet implemented');
    }
}

function getAutoCondition(thresholds, value) {
    thresholds = thresholds.slice();
    thresholds.unshift(Number.MIN_SAFE_INTEGER);
    thresholds.push(Number.MAX_SAFE_INTEGER);
    for (var i = 0; i < thresholds.length - 1; i++) {
        if (value > thresholds[i] && value <= thresholds[i + 1]) {
            return function (val) {
                return val > thresholds[i] && val <= thresholds[i + 1];
            };
        }
    }
}

function flatten(arr) {
    var res = [];
    for (var i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            for (var j = 0; j < arr[i].length; j++) {
                res.push(arr[i][j]);
            }
        } else {
            res.push(arr[i]);
        }
    }
    return res;
}

module.exports = Route;<commit_msg>split: add options argument<commit_after>'use strict';
const parseRule = require('./parseRule');
const converter = require('./unitConverter');
const SG = require('ml-savitzky-golay');
const GpxHelper = require('./GpxHelper');

const SGDefault = {
    windowSize: 31,
    pad: 'pre',
    derivative: 0
};

class Route {
    constructor(segments, options) {
        options = options || {};
        // Make a deep copy
        this.segments = segments.map(segment => {
            return {
                distance: segment.distance,
                duration: segment.duration,
                elevation: segment.elevation
            };
        });
        this._timeUnit = options.timeUnit || 'h';
        this._lengthUnit = options.distanceUnit || 'km';
        this._speedUnit = this._lengthUnit + '/' + this._timeUnit;
        this._paceUnit = this._timeUnit + '/' + this._lengthUnit;
        if(options.SG) {
            this._SG = Object.assign({}, SGDefault, options.SG);
        }
    }

    get distance() {
        return this.segments.map(segment => segment.distance);
    }

    get duration() {
        return this.segments.map(segment => segment.duration);
    }

    get elevation() {
        return this.segments.map(segment => segment.elevation);
    }

    _getComputedProperty(property) {
        var prop = '_' + property;
        if(!this[prop]) {
            this._compute();
        }
        return this[prop];
    }

    get speed() {
        return this._getComputedProperty('speed');
    }

    get elevationSpeed() {
        return this._getComputedProperty('elevationSpeed');
    }

    get meanSpeed() {
        return this._getComputedProperty('meanSpeed');
    }

    get meanPace() {
        return this._getComputedProperty('meanPace');
    }

    get totalDuration() {
        return this._getComputedProperty('totalDuration');
    }

    get totalDistance() {
        return this._getComputedProperty('totalDistance');
    }

    get totalElevation() {
        return this._getComputedProperty('totalElevation');
    }

    get totalUp() {
        return this._getComputedProperty('totalUp');
    }

    get totalDown() {
        return this._getComputedProperty('totalDown');
    }

    get cumulDuration() {
        return this._getComputedProperty('cumulDuration');
    }

    get cumulDistance() {
        return this._getComputedProperty('cumulDistance');
    }

    get cumulElevation() {
        return this._getComputedProperty('cumulElevation');
    }

    split(splitRule, options) {
        var options = Object.assign({}, options, {

        })
        var that = this;
        var distances = this.cumulDistance;
        var times = this.cumulDuration;
        var elevations = this.cumulElevation;
        var remainderD = 0;
        var remainderT = 0;
        var remainderE = 0;

        function getNextIdx(type, property, idx, val) {
            var measures, remainder;
            if (type === 'joker' && val === '*') return distances.length - 1;
            if (property === 'cumulDistance') {
                val = converter.convert(val, that._lengthUnit);
                measures = distances;
                remainder = remainderD;
            } else if (property === 'cumulDuration') {
                val = converter.convert(val, that._timeUnit);
                measures = times;
                remainder = remainderT;
            } else if (property === 'cumulElevation') {
                val = converter.convert(val, that._lengthUnit);
                measures = elevations;
                remainder = remainderE;
            }
            var i = idx;
            var comp = val - remainder;

            while (i < measures.length - 1) {
                if (comp <= measures[i] - measures[idx]) {
                    var f = (measures[i] - measures[i - 1]) / (measures[i] - measures[idx] - val + remainder);
                    if (property === 'cumulDistance') {
                        remainderD = distances[i] - distances[idx] - val + remainder;
                        remainderT = (times[i] - times[i - 1]) / f;
                        remainderE = (elevations[i] - elevations[i - 1]) / f;
                    } else if (property === 'cumulDuration') {
                        remainderT = times[i] - times[idx] - val + remainder;
                        remainderD = (distances[i] - distances[i - 1]) / f;
                        remainderE = (elevations[i] - elevations[i - 1]) / f;
                    } else {
                        remainderE = elevations[i] - elevations[idx] - val + remainder;
                        remainderT = (times[i] - times[i - 1]) / f;
                        remainderD = (distances[i] - distances[i - 1]) / f;
                    }
                    break;
                }
                i++;
            }
            return i;
        }

        function getNextAutoIdx(route, thresholds, from) {
            var to = route.segments.length - 1;
            var property = thresholds[0].property;
            var unitProperty = '_' + thresholds[0].type + 'Unit';
            // convert thresholds to matching units
            for (var i = 0; i < thresholds.length; i++) {
                thresholds[i].value = converter.convert(thresholds[i].value, route[unitProperty]);
            }
            var condition = getAutoCondition(thresholds.map(t => t.value), route[property][from]);
            ++from;
            while (condition(route[property][from]) && from <= to) {
                ++from;
            }
            return from;
        }

        var serie = parseRule(splitRule);
        var parts = [];

        var idx = 0, from;
        for (var i = 0; i < serie.length; i++) {
            from = idx;
            idx = getNextIdx(serie[i].type, serie[i].property, idx, serie[i].value);
            parts.push({from, to: idx});
        }

        var split = new Array(parts.length);
        for (var i = 0; i < parts.length; i++) {
            split[i] = Route.fromSegments(this.segments.slice(parts[i].from, parts[i].to), options);

            if (serie[i].thresholds) {
                var fromTo = [];
                from = 0;
                while (from < split[i].cumulDistance.length - 1) {
                    var thresholds = serie[i].thresholds.slice().map(obj => Object.assign({}, obj));
                    var f = from;
                    from = getNextAutoIdx(split[i], thresholds, from);
                    fromTo.push({from: f, to: from});
                }
                var subsplit = new Array(fromTo.length);
                for (var j = 0; j < fromTo.length; j++) {
                    subsplit[j] = Route.fromSegments(split[i].segments.slice(fromTo[j].from, fromTo[j].to), options);
                }
                split[i] = subsplit;
            }
        }

        return flatten(split);
    }

    setUnits(distanceUnit, timeUnit) {
        var hasChanged = false, distanceUnitChanged = false, timeUnitChanged = false;
        if (distanceUnit && distanceUnit !== this._lengthUnit) {
            hasChanged = true;
            distanceUnitChanged = true;
        }
        if (timeUnit && this._timeUnit !== timeUnit) {
            hasChanged = true;
            timeUnitChanged = true;
        }
        if (!hasChanged) return;
        if (distanceUnitChanged) {
            for (var i = 0; i < this.segments.length; i++) {
                this.segments[i].distance = converter.convert(this.segments[i].distance + this._lengthUnit, distanceUnit);
                this.segments[i].elevation = converter.convert(this.segments[i].elevation + this._lengthUnit, distanceUnit);
            }
            this._lengthUnit = distanceUnit;
        }
        if (timeUnitChanged) {
            for (i = 0; i < this.segments.length; i++) {
                this.segments[i].duration = converter.convert(this.segments[i].duration + this._timeUnit, timeUnit);
            }
            this._timeUnit = timeUnit;
        }

        this._speedUnit = this._lengthUnit + '/' + this._timeUnit;
        this._paceUnit = this._timeUnit + '/' + this._lengthUnit;
        if (hasChanged && this._computeCalled) {
            this._compute();
        }

    }

    _compute() {
        this._computeCalled = true;
        this._cumulDistance = new Array(this.segments.length);
        this._cumulDuration = new Array(this.segments.length);
        this._cumulElevation = new Array(this.segments.length);
        this._totalDistance = 0;
        this._totalDuration = 0;
        this._totalElevation = 0;
        this._totalUp = 0;
        this._totalDown = 0;
        this._meanSpeed = 0;
        this._speed = new Array(this.segments.length);
        this._elevationSpeed = new Array(this.segments.length);
        var cumSpeed = 0;

        for (var i = 0; i < this.segments.length; i++) {
            var segment = this.segments[i];
            this._speed[i] = segment.distance / segment.duration;
            this._elevationSpeed[i] = segment.elevation / segment.duration;
            this._totalDuration += segment.duration;
            this._totalDistance += segment.distance;
            this._totalElevation += segment.elevation;
            if(segment.elevation > 0) {
                this._totalUp += segment.elevation;
            } else {
                this._totalDown += segment.elevation;
            }
            this._cumulDuration [i] = this._totalDuration;
            this._cumulDistance[i] = this._totalDistance;
            this._cumulElevation[i] = this._totalElevation;
            cumSpeed += this._speed[i];
        }

        this._meanSpeed = cumSpeed / this.segments.length;
        this._meanPace = 1 / this._meanSpeed;
        this._cumulDistance.unshift(0);
        this._cumulDuration.unshift(0);
        this._cumulElevation.unshift(0);
        if(this._SG) {
            this._speed = SG(this._speed, 1, this._SG);
            this._elevationSpeed = SG(this._elevationSpeed, 1, this._SG);
        }
    }

    static fromGpx(gpx, options) {
        var gpxHelper = new GpxHelper(gpx);
        return gpxHelper.compute(0,0).then(segments => {
            return Route.fromSegments(segments, options);
        });
    }

    static fromSegments(segments, options) {
        return new Route(segments, options);
    }

    static fromRule(rule) {
        throw new Error('not yet implemented');
    }
}

function getAutoCondition(thresholds, value) {
    thresholds = thresholds.slice();
    thresholds.unshift(Number.MIN_SAFE_INTEGER);
    thresholds.push(Number.MAX_SAFE_INTEGER);
    for (var i = 0; i < thresholds.length - 1; i++) {
        if (value > thresholds[i] && value <= thresholds[i + 1]) {
            return function (val) {
                return val > thresholds[i] && val <= thresholds[i + 1];
            };
        }
    }
}

function flatten(arr) {
    var res = [];
    for (var i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            for (var j = 0; j < arr[i].length; j++) {
                res.push(arr[i][j]);
            }
        } else {
            res.push(arr[i]);
        }
    }
    return res;
}

module.exports = Route;<|endoftext|>


<commit_before>import THREE from 'three';
import Mesh from './Mesh';
import Iso from './Iso';

export default class Scene {
  threeScene = new THREE.Scene();
  camera = null;

  constructor(container:HTMLElement) {
    // Fog
    this.threeScene.fog = new THREE.FogExp2(0x000000, 5);

    // Ambient light
    this.ambientLight = new THREE.AmbientLight(0x404040, 2);
    this.threeScene.add(this.ambientLight);

    // Directional light
    var light = new THREE.DirectionalLight(0xffffff, 1);
    light.position.set(-30, 40, 20);

    light.castShadow = true;
    light.shadow.mapSize = new THREE.Vector2(50, 50);
    var size = 50;
    light.shadow.camera.left = -size;
    light.shadow.camera.right = size;
    light.shadow.camera.top = -size;
    light.shadow.camera.bottom = size;
    light.shadow.camera.near = 10;
    light.shadow.camera.far = 100;

    this.threeScene.add(light);

    // light helper
    if (Iso.DEBUG) {
      this.threeScene.add(new THREE.DirectionalLightHelper(light, 0));
      this.threeScene.add(new THREE.CameraHelper(light.shadow.camera));
    }

    // axes
    if (Iso.DEBUG) {
      this.threeScene.add(new THREE.AxisHelper(40));
    }

    // grid
    if (Iso.DEBUG) {
      let geometry = new THREE.PlaneBufferGeometry(100, 100, 10, 10);
      let material = new THREE.MeshBasicMaterial({
        wireframe: true,
        opacity: 0.5,
        transparent: true,
      });
      let grid = new THREE.Mesh(geometry, material);
      grid.rotation.order = 'YXZ';
      grid.rotation.y = -Math.PI / 2;
      grid.rotation.x = -Math.PI / 2;
      this.threeScene.add(grid);
    }

    this.setupCamera(container);
  }

  /**
   * Adds a mesh to the scene
   * @param {Mesh} mesh
   * @returns {Scene}
   */
  add(mesh:Mesh):Scene {
    this.threeScene.add(mesh.threeMesh);
    return this;
  }

  /**
   * Sets the scene's ambient light color
   * @param color Light's color
   * @param intensity (optional) Light's intensity
   * @returns {Scene}
   */
  setAmbientLight(color:Iso.Color, intensity:?number):Scene {
    this.ambientLight.color = color;
    if (intensity !== undefined) {
      this.ambientLight.intensity = intensity;
    }
    return this;
  }

  setupCamera(container:HTMLElement) {
    // Setup camera
    this.camera = new Iso.Camera({
      x: 10,
      z: 10,
      zoom: 3,
    }, container);
    return this;
  }

  /**
   * World resize callback.
   * @returns {Scene}
   */
  resize():Scene {
    this.camera.resize();
    return this;
  }

  clear():Scene {
    let meshes = this.threeScene.children.filter(c => c instanceof THREE.Mesh);
    for (let child of meshes) {
      this.threeScene.remove(child);
    }
    return this;
  }
}<commit_msg>Add Scene#remove<commit_after>import THREE from 'three';
import Mesh from './Mesh';
import Iso from './Iso';

export default class Scene {
  threeScene = new THREE.Scene();
  camera = null;

  constructor(container:HTMLElement) {
    // Fog
    this.threeScene.fog = new THREE.FogExp2(0x000000, 5);

    // Ambient light
    this.ambientLight = new THREE.AmbientLight(0x404040, 2);
    this.threeScene.add(this.ambientLight);

    // Directional light
    var light = new THREE.DirectionalLight(0xffffff, 1);
    light.position.set(-30, 40, 20);

    light.castShadow = true;
    light.shadow.mapSize = new THREE.Vector2(50, 50);
    var size = 50;
    light.shadow.camera.left = -size;
    light.shadow.camera.right = size;
    light.shadow.camera.top = -size;
    light.shadow.camera.bottom = size;
    light.shadow.camera.near = 10;
    light.shadow.camera.far = 100;

    this.threeScene.add(light);

    // light helper
    if (Iso.DEBUG) {
      this.threeScene.add(new THREE.DirectionalLightHelper(light, 0));
      this.threeScene.add(new THREE.CameraHelper(light.shadow.camera));
    }

    // axes
    if (Iso.DEBUG) {
      this.threeScene.add(new THREE.AxisHelper(40));
    }

    // grid
    if (Iso.DEBUG) {
      let geometry = new THREE.PlaneBufferGeometry(100, 100, 10, 10);
      let material = new THREE.MeshBasicMaterial({
        wireframe: true,
        opacity: 0.5,
        transparent: true,
      });
      let grid = new THREE.Mesh(geometry, material);
      grid.rotation.order = 'YXZ';
      grid.rotation.y = -Math.PI / 2;
      grid.rotation.x = -Math.PI / 2;
      this.threeScene.add(grid);
    }

    this.setupCamera(container);
  }

  /**
   * Adds a mesh to the scene
   * @param {Mesh} mesh
   * @returns {Scene}
   */
  add(mesh:Mesh):Scene {
    this.threeScene.add(mesh.threeMesh);
    return this;
  }

  /**
   * Remove a mesh from the scene
   * @param {Mesh} mesh
   * @returns {Scene}
   */
  remove(mesh:Mesh):Scene {
    this.threeScene.remove(mesh.threeMesh);
    return this;
  }

  /**
   * Sets the scene's ambient light color
   * @param color Light's color
   * @param intensity (optional) Light's intensity
   * @returns {Scene}
   */
  setAmbientLight(color:Iso.Color, intensity:?number):Scene {
    this.ambientLight.color = color;
    if (intensity !== undefined) {
      this.ambientLight.intensity = intensity;
    }
    return this;
  }

  setupCamera(container:HTMLElement) {
    // Setup camera
    this.camera = new Iso.Camera({
      x: 10,
      z: 10,
      zoom: 3,
    }, container);
    return this;
  }

  /**
   * World resize callback.
   * @returns {Scene}
   */
  resize():Scene {
    this.camera.resize();
    return this;
  }

  clear():Scene {
    let meshes = this.threeScene.children.filter(c => c instanceof THREE.Mesh);
    for (let child of meshes) {
      this.threeScene.remove(child);
    }
    return this;
  }
}<|endoftext|>


<commit_before>/**
* Holds the following functions:
* linkClickListener(event) - event listener for the Window's click event
* onPopupShowing(event) - event listener for the context menu's popup showing event.
* init() - adds the above event listeners to their appropriate events.
*/
var vlcMenuItem = {

	/**
	* Used as an Event Listener for the window element on "load".
	* Adds the onPopupShowing event listener to the context menu's popupShowing event
	* and linkClickListener event listener to the window's click event
	*/
	init : function() {
		var contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
		if (contentAreaContextMenu)
			contentAreaContextMenu.addEventListener("popupshowing", vlcMenuItem.onPopupShowing, false);

		window.addEventListener("click", vlcMenuItem.linkClickListener, false);
	},

	/**
	* Link click listener. Listens for the middle mouse click on youtube link
	* (that do not contain only '#' in the href)
	*/
	linkClickListener : function(e) {
		//Is middle mouse button clicked
		if(e.which !== 2)
			return;

		var link = $(e.target).closest('a');
		if(!link.length || link.attr('href') === '#')
			return;

		var url = link[0].href;
		//Is it a youtube url
		if(ytVidId(url) === false)
			return;

		var prefs = Components.classes["@mozilla.org/preferences-service;1"]
			.getService(Components.interfaces.nsIPrefService)
			.getBranch("extensions.vlc_shortcut.");
		var action;

		if(e.ctrlKey)
			action = prefs.getIntPref("ctrlmiddleclick");
		else if(e.shiftKey)
			action = prefs.getIntPref("shiftmiddleclick");
		else
			action = prefs.getIntPref("middleclick");

		if(action === 1){
			//Play video
			e.preventDefault();
			e.stopPropagation();
			vlcProcess.playVideo(url);
		}
		else if (action === 2) {
			//Enqueue video
			e.preventDefault();
			e.stopPropagation();
			vlcProcess.enqueueVideo(url);
		}
	},

	/**
	* Used as an Event Listener for the context menu on "popupshowing".
	* Initialises the "hidden" state of the context-menu buttons
	* depending on the location of the context-menu click.
	*/
	onPopupShowing : function(event) {
		var vlc_play = document.getElementById("vlc_play");
		var vlc_enqueue = document.getElementById("vlc_enqueue");
		var vlc_playLink = document.getElementById("vlc_play_link");
		var vlc_enqueueLink = document.getElementById("vlc_enqueue_link");

		var youtubeURL = false;

		//Is the context menu created on a youtube link
		if (gContextMenu.linkURL) {
			youtubeURL = ytVidId(gContextMenu.linkURL);
			if (youtubeURL === false) {
				vlc_playLink.hidden = true;
				vlc_enqueueLink.hidden = true;
			}
			else {
				vlc_playLink.hidden = false;
				vlc_enqueueLink.hidden = false;
			}
		}
		else {
			vlc_playLink.hidden = true;
			vlc_enqueueLink.hidden = true;
		}

		//Is the current page a youtube video
		youtubeURL = ytVidId(gBrowser.contentDocument.location.href);
		if (youtubeURL === false) {
			vlc_play.hidden = true;
			vlc_enqueue.hidden = true;
		}
		else {
			vlc_play.hidden = false;
			vlc_enqueue.hidden = false;
		}
	}
}

/**
* Holds the following functions:
* initProcess() - initializes the information for the VLC process needed for running the player.
* playVideo(url) - plays the video from the URL
* enqueueVideo(url) - enqueues the video from the URL
* getPlayerFilepath() - gets the filepath of the player, either from the preferences, or from a default location (depends on OS).
*/
var vlcProcess = {

	/**
	* Initialises the process to call.
	* Adds the command path to the process
	*/
	initProcess : function() {
		vlcProcess.process = Components.classes["@mozilla.org/process/util;1"]
			.createInstance(Components.interfaces.nsIProcess);
		vlcProcess.file = Components.classes["@mozilla.org/file/local;1"]
			.createInstance(Components.interfaces.nsILocalFile);

		var path = vlcProcess.getPlayerFilepath();
		vlcProcess.file.initWithPath(path);

		if(vlcProcess.file.exists()){
			vlcProcess.process.init(vlcProcess.file);
		}
		else {
			var title = "VLC Shortcut";
			var stringBundle = Components.classes["@mozilla.org/intl/stringbundle;1"]
                                    .getService(Components.interfaces.nsIStringBundleService)
                                    .createBundle('chrome://vlc_shortcut/locale/vlc_shortcut.properties');
			var message = stringBundle.GetStringFromName("error.playerNotFound");
			var prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService);
			prompts.alert(null, title, message);
			return false;
		}

		return true;
	},

	playVideo : function(url) {
		var initSuccessful = vlcProcess.initProcess();
		if(!initSuccessful){
			return;
		}

		var args = [];
		args[0] = "--one-instance";

		url = vlcProcess.changeHttpsToHttp(url);
		args[1] = url;

		try {
			vlcProcess.process.run(false, args, args.length);
		} catch(err) {
			var errorMsg = "Error: " + err.message;
			alert(errorMsg);
		}

	},

	enqueueVideo : function(url) {
		var initSuccessful = vlcProcess.initProcess();
		if(!initSuccessful){
			return;
		}

		var args = [];
		args[0] = "--one-instance";
		args[1] = "--playlist-enqueue";

		url = vlcProcess.changeHttpsToHttp(url);
		args[2] = url;

		try {
			vlcProcess.process.run(false, args, args.length);
		} catch(err) {
			var errorMsg = "Error: " + err.message;
			alert(errorMsg);
		}
	},

	getPlayerFilepath : function() {
		var prefsBranch = Components.classes["@mozilla.org/preferences-service;1"]
			.getService(Components.interfaces.nsIPrefService)
			.getBranch("extensions.vlc_shortcut.");
		var filepath = prefsBranch.getCharPref("vlc_filepath");
		if(filepath === ""){
			var OS = Services.appinfo.OS;
			if(OS == "Linux"){
				filepath = "/usr/bin/vlc";
			}
			else if(OS == "WINNT"){

				var file = Components.classes["@mozilla.org/file/local;1"]
						.createInstance(Components.interfaces.nsILocalFile);
				//trying path for 32-bit Windows
				file.initWithPath("C:\\Program Files\\VideoLAN\\VLC\\vlc.exe");
				if(file.exists()){
					filepath = "C:\\Program Files\\VideoLan\\VLC\\vlc.exe";
				}
				//trying path for 64-bit Windows
				else {
					file.initWithPath("C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe");
					if(file.exists()){
						filepath = "C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe";
					}
				}
			}
			else if(OS == "Darwin"){
				var file = Components.classes["@mozilla.org/file/local;1"]
						.createInstance(Components.interfaces.nsILocalFile);

				file.initWithPath("/Applications/VLC.app/Contents/MacOS/VLC");
				if(file.exists()){
					filepath = "/Applications/VLC.app/Contents/MacOS/VLC";
				}
			}
			prefsBranch.setCharPref("vlc_filepath", filepath);
		}
		return filepath;
	},

	changeHttpsToHttp : function(url) {
		//Getting rid of the https part of the URL, because VLC does not process it well.
		//Thanks to Mozilla user 'lunchboffin'
		if(url.substring(0, 5) == 'https')
			url = 'http' + url.substring(5);

		return url;
	}
}

/**
 * JavaScript function to match (and return) the video Id
 * of any valid Youtube Url, given as input string.
 * @author: Stephan Schmitz <eyecatchup@gmail.com>
 * @url: http://stackoverflow.com/a/10315969/624466
 * modified to include m.youtube and youtube-nocookie.com domains
 */
function ytVidId(url) {
	var ytRegex = /^(?:https?:\/\/)?(?:(?:www|m)\.)?(?:youtu\.be\/|youtube(?:-nocookie)?\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
	return (url.match(ytRegex)) ? RegExp.$1 : false;
}

window.addEventListener("load", vlcMenuItem.init, false);
<commit_msg>fixed vlc args for darwin<commit_after>/**
* Holds the following functions:
* linkClickListener(event) - event listener for the Window's click event
* onPopupShowing(event) - event listener for the context menu's popup showing event.
* init() - adds the above event listeners to their appropriate events.
*/
var vlcMenuItem = {

	/**
	* Used as an Event Listener for the window element on "load".
	* Adds the onPopupShowing event listener to the context menu's popupShowing event
	* and linkClickListener event listener to the window's click event
	*/
	init : function() {
		var contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
		if (contentAreaContextMenu)
			contentAreaContextMenu.addEventListener("popupshowing", vlcMenuItem.onPopupShowing, false);

		window.addEventListener("click", vlcMenuItem.linkClickListener, false);
	},

	/**
	* Link click listener. Listens for the middle mouse click on youtube link
	* (that do not contain only '#' in the href)
	*/
	linkClickListener : function(e) {
		//Is middle mouse button clicked
		if(e.which !== 2)
			return;

		var link = $(e.target).closest('a');
		if(!link.length || link.attr('href') === '#')
			return;

		var url = link[0].href;
		//Is it a youtube url
		if(ytVidId(url) === false)
			return;

		var prefs = Components.classes["@mozilla.org/preferences-service;1"]
			.getService(Components.interfaces.nsIPrefService)
			.getBranch("extensions.vlc_shortcut.");
		var action;

		if(e.ctrlKey)
			action = prefs.getIntPref("ctrlmiddleclick");
		else if(e.shiftKey)
			action = prefs.getIntPref("shiftmiddleclick");
		else
			action = prefs.getIntPref("middleclick");

		if(action === 1){
			//Play video
			e.preventDefault();
			e.stopPropagation();
			vlcProcess.playVideo(url);
		}
		else if (action === 2) {
			//Enqueue video
			e.preventDefault();
			e.stopPropagation();
			vlcProcess.enqueueVideo(url);
		}
	},

	/**
	* Used as an Event Listener for the context menu on "popupshowing".
	* Initialises the "hidden" state of the context-menu buttons
	* depending on the location of the context-menu click.
	*/
	onPopupShowing : function(event) {
		var vlc_play = document.getElementById("vlc_play");
		var vlc_enqueue = document.getElementById("vlc_enqueue");
		var vlc_playLink = document.getElementById("vlc_play_link");
		var vlc_enqueueLink = document.getElementById("vlc_enqueue_link");

		var youtubeURL = false;

		//Is the context menu created on a youtube link
		if (gContextMenu.linkURL) {
			youtubeURL = ytVidId(gContextMenu.linkURL);
			if (youtubeURL === false) {
				vlc_playLink.hidden = true;
				vlc_enqueueLink.hidden = true;
			}
			else {
				vlc_playLink.hidden = false;
				vlc_enqueueLink.hidden = false;
			}
		}
		else {
			vlc_playLink.hidden = true;
			vlc_enqueueLink.hidden = true;
		}

		//Is the current page a youtube video
		youtubeURL = ytVidId(gBrowser.contentDocument.location.href);
		if (youtubeURL === false) {
			vlc_play.hidden = true;
			vlc_enqueue.hidden = true;
		}
		else {
			vlc_play.hidden = false;
			vlc_enqueue.hidden = false;
		}
	}
}

/**
* Holds the following functions:
* initProcess() - initializes the information for the VLC process needed for running the player.
* playVideo(url) - plays the video from the URL
* enqueueVideo(url) - enqueues the video from the URL
* getPlayerFilepath() - gets the filepath of the player, either from the preferences, or from a default location (depends on OS).
*/
var vlcProcess = {

	/**
	* Initialises the process to call.
	* Adds the command path to the process
	*/
	initProcess : function() {
		vlcProcess.process = Components.classes["@mozilla.org/process/util;1"]
			.createInstance(Components.interfaces.nsIProcess);
		vlcProcess.file = Components.classes["@mozilla.org/file/local;1"]
			.createInstance(Components.interfaces.nsILocalFile);

		var path = vlcProcess.getPlayerFilepath();
		vlcProcess.file.initWithPath(path);

		if(vlcProcess.file.exists()){
			vlcProcess.process.init(vlcProcess.file);
		}
		else {
			var title = "VLC Shortcut";
			var stringBundle = Components.classes["@mozilla.org/intl/stringbundle;1"]
                                    .getService(Components.interfaces.nsIStringBundleService)
                                    .createBundle('chrome://vlc_shortcut/locale/vlc_shortcut.properties');
			var message = stringBundle.GetStringFromName("error.playerNotFound");
			var prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].getService(Components.interfaces.nsIPromptService);
			prompts.alert(null, title, message);
			return false;
		}

		return true;
	},

	playVideo : function(url) {
		var prefsh = Components.classes["@mozilla.org/preferences-service;1"]
			.getService(Components.interfaces.nsIPrefService)
			.getBranch("extensions.vlc_shortcut.");
		var selectedValue = prefsh.getIntPref("vq");
		var initSuccessful = vlcProcess.initProcess();
		if(!initSuccessful){
			return;
		}

		var args = [];
		var OS = Services.appinfo.OS;
		if(OS == "Darwin"){
			if(selectedValue){
				args[0] = "--preferred-resolution=" + selectedValue.toString();
				url = vlcProcess.changeHttpsToHttp(url);
				args[1] = url;
			}else{
				url = vlcProcess.changeHttpsToHttp(url);
				args[0] = url;
			}
		}else{
			if(selectedValue){
				args[0] = "--one-instance";
				args[1] = "--preferred-resolution=" + selectedValue.toString();
				url = vlcProcess.changeHttpsToHttp(url);
				args[2] = url;
			}else{
				args[0] = "--one-instance";
				url = vlcProcess.changeHttpsToHttp(url);
				args[1] = url;
			}
		}

		try {
			vlcProcess.process.run(false, args, args.length);
		} catch(err) {
			var errorMsg = "Error: " + err.message;
			alert(errorMsg);
		}

	},

	enqueueVideo : function(url) {
		var initSuccessful = vlcProcess.initProcess();
		if(!initSuccessful){
			return;
		}

		var args = [];
		var OS = Services.appinfo.OS;
		if(OS == "Darwin"){
			url = vlcProcess.changeHttpsToHttp(url);
			args[0] = url;
		}else{
			args[0] = "--one-instance";
			args[1] = "--playlist-enqueue";

			url = vlcProcess.changeHttpsToHttp(url);
			args[2] = url;
		}

		try {
			vlcProcess.process.run(false, args, args.length);
		} catch(err) {
			var errorMsg = "Error: " + err.message;
			alert(errorMsg);
		}
	},

	getPlayerFilepath : function() {
		var prefsBranch = Components.classes["@mozilla.org/preferences-service;1"]
			.getService(Components.interfaces.nsIPrefService)
			.getBranch("extensions.vlc_shortcut.");
		var filepath = prefsBranch.getCharPref("vlc_filepath");
		if(filepath === ""){
			var OS = Services.appinfo.OS;
			if(OS == "Linux"){
				filepath = "/usr/bin/vlc";
			}
			else if(OS == "WINNT"){

				var file = Components.classes["@mozilla.org/file/local;1"]
						.createInstance(Components.interfaces.nsILocalFile);
				//trying path for 32-bit Windows
				file.initWithPath("C:\\Program Files\\VideoLAN\\VLC\\vlc.exe");
				if(file.exists()){
					filepath = "C:\\Program Files\\VideoLan\\VLC\\vlc.exe";
				}
				//trying path for 64-bit Windows
				else {
					file.initWithPath("C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe");
					if(file.exists()){
						filepath = "C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe";
					}
				}
			}
			else if(OS == "Darwin"){
				var file = Components.classes["@mozilla.org/file/local;1"]
						.createInstance(Components.interfaces.nsILocalFile);

				file.initWithPath("/Applications/VLC.app/Contents/MacOS/VLC");
				if(file.exists()){
					filepath = "/Applications/VLC.app/Contents/MacOS/VLC";
				}
			}
			prefsBranch.setCharPref("vlc_filepath", filepath);
		}
		return filepath;
	},

	changeHttpsToHttp : function(url) {
		//Getting rid of the https part of the URL, because VLC does not process it well.
		//Thanks to Mozilla user 'lunchboffin'
		if(url.substring(0, 5) == 'https')
			url = 'http' + url.substring(5);

		return url;
	}
}

/**
 * JavaScript function to match (and return) the video Id
 * of any valid Youtube Url, given as input string.
 * @author: Stephan Schmitz <eyecatchup@gmail.com>
 * @url: http://stackoverflow.com/a/10315969/624466
 * modified to include m.youtube and youtube-nocookie.com domains
 */
function ytVidId(url) {
	var ytRegex = /^(?:https?:\/\/)?(?:(?:www|m)\.)?(?:youtu\.be\/|youtube(?:-nocookie)?\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
	return (url.match(ytRegex)) ? RegExp.$1 : false;
}

window.addEventListener("load", vlcMenuItem.init, false);
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var fs = require('fs'),
    path = require('path'),
    util = require('util');

var BIDI_RTL_LANGS = ['ar', 'fa', 'he'],
    translations = {},
    default_lang = 'en-US',
    default_locale = 'en_US';

function gettext(sid, locale) {
  if (translations[locale][sid] && translations[locale][sid].length) {
    return translations[locale][sid];
  }
  // Return the default lang's string if missing in the translation.
  return (translations[default_locale][sid]) || sid;
}

function qualityCmp(a, b) {
  if (a.quality === b.quality) {
    return 0;
  } else if (a.quality < b.quality) {
    return 1;
  } else {
    return -1;
  }
}

/**
 * Parses the HTTP accept-language header and returns a
 * sorted array of objects. Example object:
 * {
 *   lang: 'pl', quality: 0.7
 * }
 **/
function parseAcceptLanguage(header) {
  // pl,fr-FR;q=0.3,en-US;q=0.1
  if (! header || ! header.split) {
    return [];
  }
  var raw_langs = header.split(',');
  var langs = raw_langs.map(function(raw_lang) {
    var parts = raw_lang.split(';');
    var q = 1;
    if (parts.length > 1 && parts[1].indexOf('q=') === 0) {
      var qval = parseFloat(parts[1].split('=')[1]);
      if (isNaN(qval) === false) {
        q = qval;
      }
    }
    return { lang: parts[0].trim(), quality: q };
  });
  langs.sort(qualityCmp);
  return langs;
}

/**
 * Given the user's prefered languages and a list of currently
 * supported languages, returns the best match or a default language.
 * languages must be a sorted list, the first match is returned.
 **/
function bestLanguage(languages, supported_languages, defaultLanguage) {
  var lower = supported_languages.map(function(l) { return l.toLowerCase(); });
  for(var i=0; i < languages.length; i++) {
    var lq = languages[i];
    if (lower.indexOf(lq.lang.toLowerCase()) !== -1) {
      return lq.lang;
    // Issue#1128 match locale, even if region isn't supported
    } else if (lower.indexOf(lq.lang.split('-')[0].toLowerCase()) !== -1) {
      return lq.lang.split('-')[0];
    }
  }
  return defaultLanguage;
}

/**
 * Given a language code, return a locale code the OS understands.
 *
 * language: en-US
 * locale:   en_US
 **/
function localeFrom(language) {
  if (! language || ! language.split) {
    return "";
  }
  var parts = language.split('-');
  if (parts.length === 1) {
    return parts[0].toLowerCase();
  } else if (parts.length === 2) {
    return util.format('%s_%s', parts[0].toLowerCase(), parts[1].toUpperCase());
  } else if (parts.length === 3) {
    // sr-Cyrl-RS should be sr_RS
    return util.format('%s_%s', parts[0].toLowerCase(), parts[2].toUpperCase());
  } else {
    console.error(
      util.format("Unable to map a local from language code [%s]", language));
    return language;
  }
}

/**
 * Given a locale code, return a language code
 **/
function languageFrom(locale) {
  if (!locale || !locale.split) {
    return "";
  }
  var parts = locale.split('_');
  if (parts.length === 1) {
    return parts[0].toLowerCase();
  } else if (parts.length === 2) {
    return util.format('%s-%s', parts[0].toLowerCase(), parts[1].toUpperCase());
  } else if (parts.length === 3) {
    // sr_RS should be sr-RS
    return util.format('%s-%s', parts[0].toLowerCase(), parts[2].toUpperCase());
  } else {
    console.error(util.format("Unable to map a language from locale code [%s]", locale));
    return locale;
  }
}

/**
 * The format function provides string interpolation on the client and server side.
 * It can be used with either an object for named variables, or an array
 * of values for positional replacement.
 *
 * Named Example:
 * format("%(salutation)s %(place)s", {salutation: "Hello", place: "World"}, true);
 * Positional Example:
 * format("%s %s", ["Hello", "World"]);
 **/
exports.format = format = function(fmt, obj, named) {
  if (!fmt) return "";
  if (Array.isArray(obj) || named === false) {
    return fmt.replace(/%s/g, function(){return String(obj.shift());});
  } else if (typeof obj === 'object' || named === true) {
    return fmt.replace(/%\(\s*([^)]+)\s*\)s/g, function(m, v){
      return String(obj[v.trim()]);
    });
  } else {
    return fmt;
  }
};

/**
 * Returns the list of translations abide is currently configured to support.
 **/
exports.getLocales = function() {
  return Object.keys(translations);
};

/**
 * Returns a copy of the translated strings for the given language.
 **/
function getStrings(lang) {
  var locale = localeFrom(lang),
      strings = {};
  if (!translations[locale]) {
    return strings;
  }

  // Copy the translation pairs and return. We copy vs. simply returning
  // so we can maintain the translations internally, and count on them.
  // In order to get all strings (including those that exist in the default
  // lang but not a translation), we use the keys from the default lang.
  Object.keys(translations[default_locale]).forEach(function(key) {
    strings[key] = gettext(key, locale);
  });
  return strings;
};
exports.getStrings = getStrings;

/**
 * A route servers can use to expose strings for a given lang:
 *
 *   app.get( "/strings/:lang?", i18n.stringsRoute( "en-US" ) );
 */
exports.stringsRoute = function(defaultLang) {
  defaultLang = defaultLang || "en-US";
  return function(req, res) {
    res.jsonp( getStrings( req.params.lang || req.lang || defaultLang ) );
  };
};

/**
 * Middleware function for Express web apps, which deals with locales on
 * headers or URLs, and provides `gettext` and `format` to other middleware functions.
 */
exports.middleware = function(options) {
  options = options || {};
  options.supported_languages = options.supported_languages || ['en-US'];
  options.translation_directory = options.translation_directory || 'locale/';
  options.locale_on_url = !options.locale_on_url ? false : true;

  default_lang = options.default_lang || 'en-US';
  default_locale = localeFrom(options.default_lang);
  default_lang = options.default_lang;

  function messages_file_path(locale) {
    return path.resolve(path.join(__dirname, '..', '..', '..'),
                        options.translation_directory,
                        path.join(locale, 'messages.json'));
  }

  function parse_messages_file(locale) {
    return require(messages_file_path(locale));
  }

  options.supported_languages.forEach(function(lang) {
    var locale = localeFrom(lang);

    try {
      translations[locale] = parse_messages_file(locale);
    } catch (e) {
      var msg = util.format(
        'Bad locale=[%s] missing .json files in [%s]. See locale/README (%s)',
        locale, messages_file_path(locale), e
      );
      console.error(msg);
      throw msg;
    }
  });

  function checkUrlLocale(req) {
    if (!options.locale_on_url) {
      return;
    }

    // Given a URL, http://foo.com/ab/xyz/, we check to see if the first directory
    // is actually a locale we know about, and if so, we strip it out of the URL
    // (i.e., URL becomes http://foo.com/xyz/) and store that locale info on the
    // request's accept-header.
    var matches = req.url.match(/^\/([^\/]+)(\/|$)/);
    if (!(matches && matches[1])) {
      return;
    }

    // Look for a lang we know about, and if found, strip it off the URL so routes
    // continue to work. If we don't find it (i.e., comes back "unknown") then bail.
    // We do this so that we don't falsely consume more of the URL than we should
    // and stip things that aren't actually locales we know about.
    var lang = bestLanguage(parseAcceptLanguage(matches[1]),
                            options.supported_languages,
                            "unknown");
    if (lang === "unknown") {
      return;
    }

    req.url = req.url.replace(matches[0], '/');
    req.headers['accept-language'] = lang;
  }

  return function(req, resp, next) {
    checkUrlLocale(req);

    var langs = parseAcceptLanguage(req.headers['accept-language']),
        lang_dir,
        lang = bestLanguage(langs, options.supported_languages,
                            options.default_lang),
        locale,
        locals = {},
        gt;

    locals.lang = lang;

    // BIDI support, which direction does text flow?
    lang_dir = BIDI_RTL_LANGS.indexOf(lang) >= 0 ? 'rtl' : 'ltr';
    locals.lang_dir = lang_dir;
    req.lang = lang;

    locale = localeFrom(lang);

    locals.locale = locale;
    req.locale = locale;

    var formatFnName = 'format';
    if (!! locals.format || !! req.format) {
      if (!! options.format_fn_name) {
        formatFnName = options.format_fn_name;
      } else {
        console.error("It appears you are using middleware which " +
          "already sets a variable 'format' on either the request " +
          "or reponse. Please use format_fn_name in options to " +
          "override this setting.");
        throw new Error("Bad Config - override format_fn_name");
      }

    }
    locals[formatFnName] = format;
    req[formatFnName] = format;

    locals.setLocale = function(assignedLocale) {
      if (translations[assignedLocale]) {
        locale = assignedLocale;

        var newLocals = {};

        newLocals.locale = assignedLocale;
        req.locale = assignedLocale;

        newLocals.lang = languageFrom(assignedLocale);
        req.lang = newLocals.lang;

        newLocals.lang_dir = BIDI_RTL_LANGS.indexOf(newLocals.lang) >= 0 ? 'rtl' : 'ltr';
        req.lang_dir = newLocals.lang_dir;

        resp.locals(newLocals);
      }
    };
    req.setLocale = locals.setLocale;

    if (translations[locale]) {
      gt = function(sid) {
        return gettext(sid, locale);
      };
    } else {
      // default lang in a non gettext environment... fake it
      gt = function(a) { return a; };
    }
    locals.gettext = gt;
    req.gettext = gt;

    // resp.locals(string, value) doesn't seem to work with EJS
    resp.locals(locals);

    next();
  };
};
<commit_msg>Fix override of default_locale and use of default_lang vs. options.default_lang<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var fs = require('fs'),
    path = require('path'),
    util = require('util');

var BIDI_RTL_LANGS = ['ar', 'fa', 'he'],
    translations = {},
    default_lang = 'en-US',
    default_locale = 'en_US';

function gettext(sid, locale) {
  if (translations[locale][sid] && translations[locale][sid].length) {
    return translations[locale][sid];
  }
  // Return the default lang's string if missing in the translation.
  return (translations[default_locale][sid]) || sid;
}

function qualityCmp(a, b) {
  if (a.quality === b.quality) {
    return 0;
  } else if (a.quality < b.quality) {
    return 1;
  } else {
    return -1;
  }
}

/**
 * Parses the HTTP accept-language header and returns a
 * sorted array of objects. Example object:
 * {
 *   lang: 'pl', quality: 0.7
 * }
 **/
function parseAcceptLanguage(header) {
  // pl,fr-FR;q=0.3,en-US;q=0.1
  if (! header || ! header.split) {
    return [];
  }
  var raw_langs = header.split(',');
  var langs = raw_langs.map(function(raw_lang) {
    var parts = raw_lang.split(';');
    var q = 1;
    if (parts.length > 1 && parts[1].indexOf('q=') === 0) {
      var qval = parseFloat(parts[1].split('=')[1]);
      if (isNaN(qval) === false) {
        q = qval;
      }
    }
    return { lang: parts[0].trim(), quality: q };
  });
  langs.sort(qualityCmp);
  return langs;
}

/**
 * Given the user's prefered languages and a list of currently
 * supported languages, returns the best match or a default language.
 * languages must be a sorted list, the first match is returned.
 **/
function bestLanguage(languages, supported_languages, defaultLanguage) {
  var lower = supported_languages.map(function(l) { return l.toLowerCase(); });
  for(var i=0; i < languages.length; i++) {
    var lq = languages[i];
    if (lower.indexOf(lq.lang.toLowerCase()) !== -1) {
      return lq.lang;
    // Issue#1128 match locale, even if region isn't supported
    } else if (lower.indexOf(lq.lang.split('-')[0].toLowerCase()) !== -1) {
      return lq.lang.split('-')[0];
    }
  }
  return defaultLanguage;
}

/**
 * Given a language code, return a locale code the OS understands.
 *
 * language: en-US
 * locale:   en_US
 **/
function localeFrom(language) {
  if (! language || ! language.split) {
    return "";
  }
  var parts = language.split('-');
  if (parts.length === 1) {
    return parts[0].toLowerCase();
  } else if (parts.length === 2) {
    return util.format('%s_%s', parts[0].toLowerCase(), parts[1].toUpperCase());
  } else if (parts.length === 3) {
    // sr-Cyrl-RS should be sr_RS
    return util.format('%s_%s', parts[0].toLowerCase(), parts[2].toUpperCase());
  } else {
    console.error(
      util.format("Unable to map a local from language code [%s]", language));
    return language;
  }
}

/**
 * Given a locale code, return a language code
 **/
function languageFrom(locale) {
  if (!locale || !locale.split) {
    return "";
  }
  var parts = locale.split('_');
  if (parts.length === 1) {
    return parts[0].toLowerCase();
  } else if (parts.length === 2) {
    return util.format('%s-%s', parts[0].toLowerCase(), parts[1].toUpperCase());
  } else if (parts.length === 3) {
    // sr_RS should be sr-RS
    return util.format('%s-%s', parts[0].toLowerCase(), parts[2].toUpperCase());
  } else {
    console.error(util.format("Unable to map a language from locale code [%s]", locale));
    return locale;
  }
}

/**
 * The format function provides string interpolation on the client and server side.
 * It can be used with either an object for named variables, or an array
 * of values for positional replacement.
 *
 * Named Example:
 * format("%(salutation)s %(place)s", {salutation: "Hello", place: "World"}, true);
 * Positional Example:
 * format("%s %s", ["Hello", "World"]);
 **/
exports.format = format = function(fmt, obj, named) {
  if (!fmt) return "";
  if (Array.isArray(obj) || named === false) {
    return fmt.replace(/%s/g, function(){return String(obj.shift());});
  } else if (typeof obj === 'object' || named === true) {
    return fmt.replace(/%\(\s*([^)]+)\s*\)s/g, function(m, v){
      return String(obj[v.trim()]);
    });
  } else {
    return fmt;
  }
};

/**
 * Returns the list of translations abide is currently configured to support.
 **/
exports.getLocales = function() {
  return Object.keys(translations);
};

/**
 * Returns a copy of the translated strings for the given language.
 **/
function getStrings(lang) {
  var locale = localeFrom(lang),
      strings = {};
  if (!translations[locale]) {
    return strings;
  }

  // Copy the translation pairs and return. We copy vs. simply returning
  // so we can maintain the translations internally, and count on them.
  // In order to get all strings (including those that exist in the default
  // lang but not a translation), we use the keys from the default lang.
  Object.keys(translations[default_locale]).forEach(function(key) {
    strings[key] = gettext(key, locale);
  });
  return strings;
};
exports.getStrings = getStrings;

/**
 * A route servers can use to expose strings for a given lang:
 *
 *   app.get( "/strings/:lang?", i18n.stringsRoute( "en-US" ) );
 */
exports.stringsRoute = function(defaultLang) {
  defaultLang = defaultLang || default_lang;
  return function(req, res) {
    res.jsonp( getStrings( req.params.lang || req.lang || defaultLang ) );
  };
};

/**
 * Middleware function for Express web apps, which deals with locales on
 * headers or URLs, and provides `gettext` and `format` to other middleware functions.
 */
exports.middleware = function(options) {
  options = options || {};
  options.supported_languages = options.supported_languages || ['en-US'];
  options.translation_directory = options.translation_directory || 'locale/';
  options.locale_on_url = !options.locale_on_url ? false : true;

  default_lang = options.default_lang || 'en-US';
  default_locale = localeFrom(default_lang);

  function messages_file_path(locale) {
    return path.resolve(path.join(__dirname, '..', '..', '..'),
                        options.translation_directory,
                        path.join(locale, 'messages.json'));
  }

  function parse_messages_file(locale) {
    return require(messages_file_path(locale));
  }

  options.supported_languages.forEach(function(lang) {
    var locale = localeFrom(lang);

    try {
      translations[locale] = parse_messages_file(locale);
    } catch (e) {
      var msg = util.format(
        'Bad locale=[%s] missing .json files in [%s]. See locale/README (%s)',
        locale, messages_file_path(locale), e
      );
      console.error(msg);
      throw msg;
    }
  });

  function checkUrlLocale(req) {
    if (!options.locale_on_url) {
      return;
    }

    // Given a URL, http://foo.com/ab/xyz/, we check to see if the first directory
    // is actually a locale we know about, and if so, we strip it out of the URL
    // (i.e., URL becomes http://foo.com/xyz/) and store that locale info on the
    // request's accept-header.
    var matches = req.url.match(/^\/([^\/]+)(\/|$)/);
    if (!(matches && matches[1])) {
      return;
    }

    // Look for a lang we know about, and if found, strip it off the URL so routes
    // continue to work. If we don't find it (i.e., comes back "unknown") then bail.
    // We do this so that we don't falsely consume more of the URL than we should
    // and stip things that aren't actually locales we know about.
    var lang = bestLanguage(parseAcceptLanguage(matches[1]),
                            options.supported_languages,
                            "unknown");
    if (lang === "unknown") {
      return;
    }

    req.url = req.url.replace(matches[0], '/');
    req.headers['accept-language'] = lang;
  }

  return function(req, resp, next) {
    checkUrlLocale(req);

    var langs = parseAcceptLanguage(req.headers['accept-language']),
        lang_dir,
        lang = bestLanguage(langs, options.supported_languages, default_lang),
        locale,
        locals = {},
        gt;

    locals.lang = lang;

    // BIDI support, which direction does text flow?
    lang_dir = BIDI_RTL_LANGS.indexOf(lang) >= 0 ? 'rtl' : 'ltr';
    locals.lang_dir = lang_dir;
    req.lang = lang;

    locale = localeFrom(lang);

    locals.locale = locale;
    req.locale = locale;

    var formatFnName = 'format';
    if (!! locals.format || !! req.format) {
      if (!! options.format_fn_name) {
        formatFnName = options.format_fn_name;
      } else {
        console.error("It appears you are using middleware which " +
          "already sets a variable 'format' on either the request " +
          "or reponse. Please use format_fn_name in options to " +
          "override this setting.");
        throw new Error("Bad Config - override format_fn_name");
      }

    }
    locals[formatFnName] = format;
    req[formatFnName] = format;

    locals.setLocale = function(assignedLocale) {
      if (translations[assignedLocale]) {
        locale = assignedLocale;

        var newLocals = {};

        newLocals.locale = assignedLocale;
        req.locale = assignedLocale;

        newLocals.lang = languageFrom(assignedLocale);
        req.lang = newLocals.lang;

        newLocals.lang_dir = BIDI_RTL_LANGS.indexOf(newLocals.lang) >= 0 ? 'rtl' : 'ltr';
        req.lang_dir = newLocals.lang_dir;

        resp.locals(newLocals);
      }
    };
    req.setLocale = locals.setLocale;

    if (translations[locale]) {
      gt = function(sid) {
        return gettext(sid, locale);
      };
    } else {
      // default lang in a non gettext environment... fake it
      gt = function(a) { return a; };
    }
    locals.gettext = gt;
    req.gettext = gt;

    // resp.locals(string, value) doesn't seem to work with EJS
    resp.locals(locals);

    next();
  };
};
<|endoftext|>


<commit_before>// System.js plugin
function normalizeLocaleId(localeId) {
    return localeId && localeId.replace(/-/g, '_').toLowerCase();
}

// Helper for getting a prioritized, normalized list of relevant locale ids from a specific locale id.
// For instance, 'en_US' produces ['en_us', 'en']
function expandLocaleIdToPrioritizedList(localeId) {
    if (!localeId) {
        return [];
    }
    localeId = normalizeLocaleId(localeId);
    var localeIds = [ localeId ];
    while (/_[^_]+$/.test(localeId)) {
        localeId = localeId.replace(/_[^_]+$/, '');
        localeIds.push(localeId);
    }
    return localeIds;
}

function tokenizePattern(pattern) {
    if (typeof pattern !== 'string') {
        var valueString = pattern;
        try {
            valueString = JSON.stringify(pattern);
        } catch (e) {}
        throw new Error('i18nTools.tokenizePattern: Value must be a string: ' + valueString);
    }
    var tokens = [],
        fragments = pattern.split(/(\{\d+\})/);
    for (var i = 0 ; i < fragments.length ; i += 1) {
        var fragment = fragments[i];
        if (fragment.length > 0) {
            var matchPlaceHolder = fragment.match(/^\{(\d+)\}$/);
            if (matchPlaceHolder) {
                tokens.push({
                    type: 'placeHolder',
                    value: parseInt(matchPlaceHolder[1], 10)
                });
            } else {
                tokens.push({
                    type: 'text',
                    value: fragment
                });
            }
        }
    }
    return tokens;
}

function eachI18nTagInHtmlDocument(document, lambda, nestedTemplateLambda) {
    var ELEMENT_NODE = 1,
        TEXT_NODE = 3,
        queue = [document],
        i;
    while (queue.length) {
        var node = queue.shift(),
            parentNode = node.parentNode,
            nodeStillInDocument = true;
        if (parentNode && node.nodeType === ELEMENT_NODE) {
            if (node.hasAttribute && node.hasAttribute('data-i18n')) { // In IE7 the HTML node doesn't have a hasAttribute method?
                var i18nStr = node.getAttribute('data-i18n'),
                    i18nObj;

                if (i18nStr.indexOf(':') !== -1) {
                    try {
                        i18nObj = eval('({' + i18nStr + '})'); // eslint-disable-line no-eval
                    } catch (e) {
                        throw new Error('i18nTools.eachI18nTagInHtmlDocument: Error evaluating data-i18n attribute: ' + i18nStr + '\n' + e.stack);
                    }
                } else {
                    i18nObj = {text: i18nStr};
                }

                if (i18nObj.attr) {
                    var attributeNames = Object.keys(i18nObj.attr);
                    for (i = 0 ; i < attributeNames.length ; i += 1) {
                        var attributeName = attributeNames[i],
                            key = i18nObj.attr[attributeName] || null;
                        if (lambda({type: 'i18nTagAttribute', attributeName: attributeName, node: node, key: key, defaultValue: node.getAttribute(attributeName)}) === false) {
                            return;
                        }
                    }
                }

                if (typeof i18nObj.text !== 'undefined') {
                    var defaultValue = '',
                        placeHolders = [],
                        nextPlaceHolderNumber = 0;

                    for (i = 0 ; i < node.childNodes.length ; i += 1) {
                        var childNode = node.childNodes[i];
                        if (childNode.nodeType === TEXT_NODE) {
                            defaultValue += childNode.nodeValue;
                        } else {
                            defaultValue += '{' + nextPlaceHolderNumber + '}';
                            nextPlaceHolderNumber += 1;
                            placeHolders.push(childNode);
                        }
                    }
                    defaultValue = defaultValue.replace(/^[ \n\t]+|[ \n\t]+$/g, ''); // Trim leading and trailing whitespace, except non-breaking space chars
                    defaultValue = defaultValue.replace(/[ \n\t]+/g, ' '); // Compress and normalize sequences of 1+ spaces to one ' '
                    if (lambda({type: 'i18nTagText', node: node, key: i18nObj.text || null, defaultValue: defaultValue, placeHolders: placeHolders}) === false) {
                        return;
                    }
                } else {
                    // A tag with a data-i18n tag, but no language key for the text contents.
                    // Give the lambda a chance to clean up the tag anyway:
                    lambda({node: node});
                }
                if (!node.parentNode) {
                    nodeStillInDocument = false;
                    queue.unshift(parentNode);
                }
            }
            // Give the caller a chance to do something about nested <script type="text/html">...</script> templates (used by TRHTML in the browser):
            if (nestedTemplateLambda && node.nodeName.toLowerCase() === 'script' && node.getAttribute('type') === 'text/html') {
                nestedTemplateLambda(node);
            }
        }
        if (nodeStillInDocument && node.childNodes) {
            for (i = node.childNodes.length - 1 ; i >= 0 ; i -= 1) {
                queue.unshift(node.childNodes[i]);
            }
        }
    }
}

function createI18nTagReplacer(options) {
    var TEXT_NODE = 3,
        allKeysForLocale = options.allKeysForLocale,
        keepI18nAttributes = options.keepI18nAttributes,
        keepSpans = options.keepSpans;

    return function i18nTagReplacer(options) {
        var key = options.key,
            node = options.node,
            value = allKeysForLocale[key],
            removeNode = !keepSpans && options.type !== 'i18nTagAttribute' && node.nodeName.toLowerCase() === 'span' && node.attributes.length === 1;

        if (key !== null) { // An empty string or null means explicitly "do not translate"
            if (/^i18nTag/.test(options.type) && value === null || typeof value === 'undefined') {
                value = options.defaultValue || '[!' + key + '!]';
            }
            if (options.type === 'i18nTagAttribute') {
                node.setAttribute(options.attributeName, value);
            } else if (options.type === 'i18nTagText') {
                while (node.childNodes.length) {
                    node.removeChild(node.firstChild);
                }
                tokenizePattern(value).forEach(function (token) {
                    var nodeToInsert;
                    if (token.type === 'text') {
                        nodeToInsert = node.ownerDocument.createTextNode(token.value);
                    } else {
                        var placeHolder = options.placeHolders[token.value];
                        if (placeHolder) {
                            nodeToInsert = placeHolder;
                            if (nodeToInsert.parentNode) {
                                nodeToInsert = nodeToInsert.cloneNode(true);
                            }
                        } else {
                            nodeToInsert = node.ownerDocument.createTextNode('[!{' + token.value + '}!]');
                        }
                    }
                    if (removeNode) {
                        if (nodeToInsert.nodeType === TEXT_NODE && node.previousSibling && node.previousSibling.nodeType === TEXT_NODE) {
                            // Splice with previous text node
                            node.previousSibling.nodeValue += nodeToInsert.nodeValue;
                        } else {
                            node.parentNode.insertBefore(nodeToInsert, node);
                        }
                    } else {
                        node.appendChild(nodeToInsert);
                    }
                });
            }
        }
        if (removeNode) {
            node.parentNode.removeChild(node);
        } else if (!keepI18nAttributes && options.type !== 'i18nTagAttribute') {
            node.removeAttribute('data-i18n');
        }
    };
}

function createTr(localeData) {
    function tokenizePattern(pattern) {
        if (typeof pattern !== 'string') {
            var valueString = pattern;
            try {
                valueString = JSON.stringify(pattern);
            } catch (e) {}
            throw new Error('i18nTools.tokenizePattern: Value must be a string: ' + valueString);
        }
        var tokens = [],
            fragments = pattern.split(/(\{\d+\})/);
        for (var i = 0 ; i < fragments.length ; i += 1) {
            var fragment = fragments[i];
            if (fragment.length > 0) {
                var matchPlaceHolder = fragment.match(/^\{(\d+)\}$/);
                if (matchPlaceHolder) {
                    tokens.push({
                        type: 'placeHolder',
                        value: parseInt(matchPlaceHolder[1], 10)
                    });
                } else {
                    tokens.push({
                        type: 'text',
                        value: fragment
                    });
                }
            }
        }
        return tokens;
    }

    var TR = function (key, defaultValue) {
        return localeData[key] || defaultValue || '[!' + key + '!]';
    };

    TR.PAT = function (key, defaultPattern) {
        var pattern = TR(key, defaultPattern), tokens = tokenizePattern(pattern);
        return function () {
            // placeHolderValue, ...
            var placeHolderValues = arguments, renderedString = '';
            for (var i = 0; i < tokens.length; i += 1) {
                var token = tokens[i];
                if (token.type === 'placeHolder') {
                    renderedString += placeHolderValues[token.value];
                } else {
                    // token.type === 'text'
                    renderedString += token.value;
                }
            }
            return renderedString;
        };
    };

    TR.HTML = function (htmlString) {
        var div = document.createElement('div');
        div.innerHTML = htmlString;
        require('./lib/eachI18nTagInHtmlDocument')(div, require('./lib/createI18nTagReplacer')({
            allKeysForLocale: localeData,
            keepI18nAttributes: true,
            keepSpans: true
        }), function nestedTemplateHandler(node) {
            if (node.firstChild && node.firstChild.nodeType === node.TEXT_NODE) {
                // Use window.TRHTML instead of TRHTML to prevent the recursive call from being recognized as a relation:
                node.firstChild.nodeValue = window.TR.HTML(node.firstChild.nodeValue);
            }
        });
        return div.innerHTML;
    };

    return TR;
}

function gatherKeysForLocale(source, locale) {
    var prioritizedLocale = expandLocaleIdToPrioritizedList(locale);
    return Object.keys(source).reduce(function (data, key) {
        for (var i = 0 ; i < prioritizedLocale.length ; i += 1) {
            var value = source[key][prioritizedLocale[i]];
            if (typeof value !== 'undefined') {
                data[key] = value;
                break;
            }
        }
        return data;
    }, {});
}

module.exports = {
    // These are maintained here because we haven't been able to work out how
    // to use relative requires in a system.js plugin:
    tokenizePattern: tokenizePattern,
    eachI18nTagInHtmlDocument: eachI18nTagInHtmlDocument,
    createI18nTagReplacer: createI18nTagReplacer,

    // fetch: sideeffect: load relevant parts of i18n file filtered by locale
    fetch: function (load, fetch) {
        load.metadata.newAddress = load.address.replace(/\.([^.]+)\.i18n/, function (str, matchedLocale) {
            load.metadata.locale = matchedLocale;
            return '.i18n';
        });
        return fetch({address: load.metadata.newAddress, metadata: {}});
    },

    translate: function (load) {
        if (this.builder) {
            load.metadata.format = 'cjs';
            load.metadata.originalSource = load.source;
            return 'module.exports = (' + createTr.toString() + ')(' + JSON.stringify(gatherKeysForLocale(JSON.parse(load.source), load.metadata.locale)) + ');';
        }
    },

    instantiate: function (load) {
        if (!this.builder) {
            return createTr(gatherKeysForLocale(JSON.parse(load.source), load.metadata.locale));
        }
    },

    listAssets: function (loads) {
        var isSeenByNewAddress = {};
        var i18nAssets = [];
        loads.forEach(function (load) {
            if (!isSeenByNewAddress[load.metadata.newAddress]) {
                isSeenByNewAddress[load.metadata.newAddress] = true;
                i18nAssets.push({
                    url: load.metadata.newAddress,
                    source: load.metadata.originalSource,
                    type: 'i18n'
                });
            }
        });
        return i18nAssets;
    }
};
<commit_msg>fix it<commit_after>// System.js plugin
function normalizeLocaleId(localeId) {
    return localeId && localeId.replace(/-/g, '_').toLowerCase();
}

// Helper for getting a prioritized, normalized list of relevant locale ids from a specific locale id.
// For instance, 'en_US' produces ['en_us', 'en']
function expandLocaleIdToPrioritizedList(localeId) {
    if (!localeId) {
        return [];
    }
    localeId = normalizeLocaleId(localeId);
    var localeIds = [ localeId ];
    while (/_[^_]+$/.test(localeId)) {
        localeId = localeId.replace(/_[^_]+$/, '');
        localeIds.push(localeId);
    }
    return localeIds;
}

function tokenizePattern(pattern) {
    if (typeof pattern !== 'string') {
        var valueString = pattern;
        try {
            valueString = JSON.stringify(pattern);
        } catch (e) {}
        throw new Error('i18nTools.tokenizePattern: Value must be a string: ' + valueString);
    }
    var tokens = [],
        fragments = pattern.split(/(\{\d+\})/);
    for (var i = 0 ; i < fragments.length ; i += 1) {
        var fragment = fragments[i];
        if (fragment.length > 0) {
            var matchPlaceHolder = fragment.match(/^\{(\d+)\}$/);
            if (matchPlaceHolder) {
                tokens.push({
                    type: 'placeHolder',
                    value: parseInt(matchPlaceHolder[1], 10)
                });
            } else {
                tokens.push({
                    type: 'text',
                    value: fragment
                });
            }
        }
    }
    return tokens;
}

function eachI18nTagInHtmlDocument(document, lambda, nestedTemplateLambda) {
    var ELEMENT_NODE = 1,
        TEXT_NODE = 3,
        queue = [document],
        i;
    while (queue.length) {
        var node = queue.shift(),
            parentNode = node.parentNode,
            nodeStillInDocument = true;
        if (parentNode && node.nodeType === ELEMENT_NODE) {
            if (node.hasAttribute && node.hasAttribute('data-i18n')) { // In IE7 the HTML node doesn't have a hasAttribute method?
                var i18nStr = node.getAttribute('data-i18n'),
                    i18nObj;

                if (i18nStr.indexOf(':') !== -1) {
                    try {
                        i18nObj = eval('({' + i18nStr + '})'); // eslint-disable-line no-eval
                    } catch (e) {
                        throw new Error('i18nTools.eachI18nTagInHtmlDocument: Error evaluating data-i18n attribute: ' + i18nStr + '\n' + e.stack);
                    }
                } else {
                    i18nObj = {text: i18nStr};
                }

                if (i18nObj.attr) {
                    var attributeNames = Object.keys(i18nObj.attr);
                    for (i = 0 ; i < attributeNames.length ; i += 1) {
                        var attributeName = attributeNames[i],
                            key = i18nObj.attr[attributeName] || null;
                        if (lambda({type: 'i18nTagAttribute', attributeName: attributeName, node: node, key: key, defaultValue: node.getAttribute(attributeName)}) === false) {
                            return;
                        }
                    }
                }

                if (typeof i18nObj.text !== 'undefined') {
                    var defaultValue = '',
                        placeHolders = [],
                        nextPlaceHolderNumber = 0;

                    for (i = 0 ; i < node.childNodes.length ; i += 1) {
                        var childNode = node.childNodes[i];
                        if (childNode.nodeType === TEXT_NODE) {
                            defaultValue += childNode.nodeValue;
                        } else {
                            defaultValue += '{' + nextPlaceHolderNumber + '}';
                            nextPlaceHolderNumber += 1;
                            placeHolders.push(childNode);
                        }
                    }
                    defaultValue = defaultValue.replace(/^[ \n\t]+|[ \n\t]+$/g, ''); // Trim leading and trailing whitespace, except non-breaking space chars
                    defaultValue = defaultValue.replace(/[ \n\t]+/g, ' '); // Compress and normalize sequences of 1+ spaces to one ' '
                    if (lambda({type: 'i18nTagText', node: node, key: i18nObj.text || null, defaultValue: defaultValue, placeHolders: placeHolders}) === false) {
                        return;
                    }
                } else {
                    // A tag with a data-i18n tag, but no language key for the text contents.
                    // Give the lambda a chance to clean up the tag anyway:
                    lambda({node: node});
                }
                if (!node.parentNode) {
                    nodeStillInDocument = false;
                    queue.unshift(parentNode);
                }
            }
            // Give the caller a chance to do something about nested <script type="text/html">...</script> templates (used by TRHTML in the browser):
            if (nestedTemplateLambda && node.nodeName.toLowerCase() === 'script' && node.getAttribute('type') === 'text/html') {
                nestedTemplateLambda(node);
            }
        }
        if (nodeStillInDocument && node.childNodes) {
            for (i = node.childNodes.length - 1 ; i >= 0 ; i -= 1) {
                queue.unshift(node.childNodes[i]);
            }
        }
    }
}

function createI18nTagReplacer(options) {
    var TEXT_NODE = 3,
        allKeysForLocale = options.allKeysForLocale,
        keepI18nAttributes = options.keepI18nAttributes,
        keepSpans = options.keepSpans;

    return function i18nTagReplacer(options) {
        var key = options.key,
            node = options.node,
            value = allKeysForLocale[key],
            removeNode = !keepSpans && options.type !== 'i18nTagAttribute' && node.nodeName.toLowerCase() === 'span' && node.attributes.length === 1;

        if (key !== null) { // An empty string or null means explicitly "do not translate"
            if (/^i18nTag/.test(options.type) && value === null || typeof value === 'undefined') {
                value = options.defaultValue || '[!' + key + '!]';
            }
            if (options.type === 'i18nTagAttribute') {
                node.setAttribute(options.attributeName, value);
            } else if (options.type === 'i18nTagText') {
                while (node.childNodes.length) {
                    node.removeChild(node.firstChild);
                }
                tokenizePattern(value).forEach(function (token) {
                    var nodeToInsert;
                    if (token.type === 'text') {
                        nodeToInsert = node.ownerDocument.createTextNode(token.value);
                    } else {
                        var placeHolder = options.placeHolders[token.value];
                        if (placeHolder) {
                            nodeToInsert = placeHolder;
                            if (nodeToInsert.parentNode) {
                                nodeToInsert = nodeToInsert.cloneNode(true);
                            }
                        } else {
                            nodeToInsert = node.ownerDocument.createTextNode('[!{' + token.value + '}!]');
                        }
                    }
                    if (removeNode) {
                        if (nodeToInsert.nodeType === TEXT_NODE && node.previousSibling && node.previousSibling.nodeType === TEXT_NODE) {
                            // Splice with previous text node
                            node.previousSibling.nodeValue += nodeToInsert.nodeValue;
                        } else {
                            node.parentNode.insertBefore(nodeToInsert, node);
                        }
                    } else {
                        node.appendChild(nodeToInsert);
                    }
                });
            }
        }
        if (removeNode) {
            node.parentNode.removeChild(node);
        } else if (!keepI18nAttributes && options.type !== 'i18nTagAttribute') {
            node.removeAttribute('data-i18n');
        }
    };
}

function createTr(localeData) {
    function tokenizePattern(pattern) {
        if (typeof pattern !== 'string') {
            var valueString = pattern;
            try {
                valueString = JSON.stringify(pattern);
            } catch (e) {}
            throw new Error('i18nTools.tokenizePattern: Value must be a string: ' + valueString);
        }
        var tokens = [],
            fragments = pattern.split(/(\{\d+\})/);
        for (var i = 0 ; i < fragments.length ; i += 1) {
            var fragment = fragments[i];
            if (fragment.length > 0) {
                var matchPlaceHolder = fragment.match(/^\{(\d+)\}$/);
                if (matchPlaceHolder) {
                    tokens.push({
                        type: 'placeHolder',
                        value: parseInt(matchPlaceHolder[1], 10)
                    });
                } else {
                    tokens.push({
                        type: 'text',
                        value: fragment
                    });
                }
            }
        }
        return tokens;
    }

    var TR = function (key, defaultValue) {
        return localeData[key] || defaultValue || '[!' + key + '!]';
    };

    TR.PAT = function (key, defaultPattern) {
        var pattern = TR(key, defaultPattern), tokens = tokenizePattern(pattern);
        return function () {
            // placeHolderValue, ...
            var placeHolderValues = arguments, renderedString = '';
            for (var i = 0; i < tokens.length; i += 1) {
                var token = tokens[i];
                if (token.type === 'placeHolder') {
                    renderedString += placeHolderValues[token.value];
                } else {
                    // token.type === 'text'
                    renderedString += token.value;
                }
            }
            return renderedString;
        };
    };

    TR.HTML = function (htmlString) {
        var div = document.createElement('div');
        div.innerHTML = htmlString;
        eachI18nTagInHtmlDocument(div, createI18nTagReplacer({
            allKeysForLocale: localeData,
            keepI18nAttributes: true,
            keepSpans: true
        }), function nestedTemplateHandler(node) {
            if (node.firstChild && node.firstChild.nodeType === node.TEXT_NODE) {
                // Use window.TRHTML instead of TRHTML to prevent the recursive call from being recognized as a relation:
                node.firstChild.nodeValue = window.TR.HTML(node.firstChild.nodeValue);
            }
        });
        return div.innerHTML;
    };

    return TR;
}

function gatherKeysForLocale(source, locale) {
    var prioritizedLocale = expandLocaleIdToPrioritizedList(locale);
    return Object.keys(source).reduce(function (data, key) {
        for (var i = 0 ; i < prioritizedLocale.length ; i += 1) {
            var value = source[key][prioritizedLocale[i]];
            if (typeof value !== 'undefined') {
                data[key] = value;
                break;
            }
        }
        return data;
    }, {});
}

module.exports = {
    // These are maintained here because we haven't been able to work out how
    // to use relative requires in a system.js plugin:
    tokenizePattern: tokenizePattern,
    eachI18nTagInHtmlDocument: eachI18nTagInHtmlDocument,
    createI18nTagReplacer: createI18nTagReplacer,

    // fetch: sideeffect: load relevant parts of i18n file filtered by locale
    fetch: function (load, fetch) {
        load.metadata.newAddress = load.address.replace(/\.([^.]+)\.i18n/, function (str, matchedLocale) {
            load.metadata.locale = matchedLocale;
            return '.i18n';
        });
        return fetch({address: load.metadata.newAddress, metadata: {}});
    },

    translate: function (load) {
        if (this.builder) {
            load.metadata.format = 'cjs';
            load.metadata.originalSource = load.source;
            return 'module.exports = (' + createTr.toString() + ')(' + JSON.stringify(gatherKeysForLocale(JSON.parse(load.source), load.metadata.locale)) + ');';
        }
    },

    instantiate: function (load) {
        if (!this.builder) {
            return createTr(gatherKeysForLocale(JSON.parse(load.source), load.metadata.locale));
        }
    },

    listAssets: function (loads) {
        var isSeenByNewAddress = {};
        var i18nAssets = [];
        loads.forEach(function (load) {
            if (!isSeenByNewAddress[load.metadata.newAddress]) {
                isSeenByNewAddress[load.metadata.newAddress] = true;
                i18nAssets.push({
                    url: load.metadata.newAddress,
                    source: load.metadata.originalSource,
                    type: 'i18n'
                });
            }
        });
        return i18nAssets;
    }
};
<|endoftext|>