<commit_before>/**

 @author: Hans van den Akker (mysim1)
 @license NPOSL-3.0
 @copyright Bizboard, 2015

 */

import isEqual                      from 'lodash/isEqual';
import {Router}                     from '../core/Router.js';
import {provide}                    from '../utils/di/Decorators.js';
import Easing                       from 'famous/transitions/Easing.js';
import AnimationController          from 'famous-flex/AnimationController.js';

/**
 * Emits the event 'routechange' with {url,controller,controllerObject,method,keys,values} when the route has changed
 */
@provide(Router)
export class ArvaRouter extends Router {

    routes = {};
    history = [];
    decode = decodeURIComponent;
    defaultController = 'Home';
    defaultMethod = 'Index';

    constructor() {
        super();
        if (window == null) {
            return;
        }
        window.addEventListener('hashchange', this.run);

        if (window == null) {
            return;
        }

        this.routes = {};
        this.history = [];
        this.decode = decodeURIComponent;

        window.addEventListener('hashchange', this.run);
        this._setupNativeBackButtonListener();
    }

    /**
     * Sets the initial controller and method to be activated whenever the controllers are activated.
     * @param {Controller|Function|String} controller Default controller instance, controller constructor, or controller name to go to.
     * @param {String} method Default method to call in given controller.
     * @returns {void}
     */
    setDefault(controller, method = null) {

        this.defaultController = this._getControllerName(controller);

        if (method != null) {
            this.defaultMethod = method;
        }
    }

    /**
     * Sets the animation specs object for use by the famous-flex AnimationController.
     * @param {Object} specs Animation specs, keyed by target controller.
     * @returns {void}
     */
    setControllerSpecs(specs) {
        this.specs = specs;
    }

    /**
     * Triggers navigation to one of the controllers
     * @param {Controller|Function|String} controller The controller instance, controller constructor, or controller name to go to.
     * @param {String} method The method to call in given controller.
     * @param {Object} params Dictonary of key-value pairs containing named arguments (i.e. {id: 1, test: "yes"})
     * @returns {void}
     */
    go(controller, method, params = null) {

        let controllerName = this._getControllerName(controller);

        let routeRoot = controllerName
            .replace('Controller', '');

        //TODO Can we skip this code?
        if(routeRoot === this.defaultController){
            routeRoot = '';
        }

        let hash = '#' + (routeRoot.length > 0 ? '/' + routeRoot : '') + ('/' + method);
        if (params !== null) {
            for (let i = 0; i < Object.keys(params).length; i++) {
                var key = Object.keys(params)[i];
                hash += i == 0 ? '?' : '&';
                hash += (key + '=' + params[key]);
            }
        }

        if (history.pushState) {
            history.pushState(null, null, hash);
        }

        this.run();
    }

    /**
     * Registers a single controller.
     * @param {String} route Route to trigger handler on.
     * @param {Object} handlers
     * @param {Function} handler.enter Method to call on entering a route.
     * @param {Function} handler.leave Method to call on when leaving a route.
     * @returns {void}
     */
    add(route, {enter, leave}, controller) {
        let pieces = route.split('/'),
            rules = this.routes;

        for (let i = 0; i < pieces.length; ++i) {
            let piece = pieces[i],
                name = piece[0] === ':' ? ':' : piece;

            rules = rules[name] || (rules[name] = {});

            if (name === ':') {
                rules['@name'] = piece.slice(1);
            }
        }

        rules['enter'] = enter;
        rules['leave'] = leave;
        rules['controller'] = controller;

    }

    /**
     * On a route change, calls the corresponding controller method with the given parameter values.
     * @returns {Boolean} Whether the current route was successfully ran.
     */
    run() {
        let url = window.location.hash.replace('#', '');

        if (url !== '') {
            url = url.replace('/?', '?');
            url[0] === '/' && (url = url.slice(1));
            url.slice(-1) === '/' && (url = url.slice(0, -1));
        }

        let rules = this.routes,
            querySplit = url.split('?'),
            pieces = querySplit[0].split('/'),
            values = [],
            keys = [],
            method = '';
        for (let piece in pieces) {
            if (pieces[piece].indexOf('=') > -1) {
                let splitted = pieces[piece].split('=');
                pieces[piece] = splitted[0];
                querySplit.push(pieces[piece] + '=' + splitted[1]);
            }
        }

        let rule = null;
        let controller;

        // if there is no controller reference, assume we have hit the default Controller
        if (pieces.length === 1 && pieces[0].length === 0) {
            pieces[0] = this.defaultController;
            pieces.push(this.defaultMethod);
        } else if (pieces.length === 1 && pieces[0].length > 0) {
            pieces.unshift(this.defaultController);
        }

        controller = pieces[0];

        // Parse the non-query portion of the URL...
        for (let i = 0; i < pieces.length && rules; ++i) {
            let piece = this.decode(pieces[i]);
            rule = rules[piece];

            if (!rule && (rule = rules[':'])) {
                method = piece;
            }

            rules = rules[piece];
        }

        (function parseQuery(q) {
            let query = q.split('&');

            for (let i = 0; i < query.length; ++i) {
                let nameValue = query[i].split('=');

                if (nameValue.length > 1) {
                    keys.push(nameValue[0]);
                    values.push(this.decode(nameValue[1]));
                }
            }
        }).call(this, querySplit.length > 1 ? querySplit[1] : '');

        if (rule && rule['enter']) {

            /* Push current route to the history stack for later use */
            let previousRoute = this.history.length ? this.history[this.history.length - 1] : undefined;
            let currentRoute = {
                url: url,
                controller: controller,
                controllerObject: rule['controller'],
                method: method,
                keys: keys,
                values: values
            };

            if(previousRoute){
                if(currentRoute.controllerObject !== previousRoute.controllerObject){
                    this.routes[previousRoute.controller][':']['leave'](currentRoute);
                }
            }
            currentRoute.spec = previousRoute ? this._getAnimationSpec(previousRoute, currentRoute) : (this._initialSpec || {});
            this._setHistory(currentRoute);

            this._executeRoute(rule, currentRoute);

            return true;
        } else {
            console.log('Controller doesn\'t exist!');
        }

        return false;
    }

    setInitialSpec(spec) {
        this._initialSpec = spec;
    }

    setBackButtonEnabled(enabled) {
        this._backButtonEnabled = enabled;
    }

    isBackButtonEnabled() {
        return this._backButtonEnabled;
    }

    /**
     * Return the previous known route, or default route if no route stack is present
     * @returns {*}
     */
    getPreviousRoute(){
        let {history} = this;
        if (history.length > 1) {
            let {controller, method, keys, values} = history[history.length - 2];
            let inputObject = {};
            for (let i = 0; i < keys.length; i++) {
                inputObject[keys[i]] = values[i];
            }
           return {controller: controller, method: method, parameters: inputObject};
        } else {
            return {controller: this.defaultController, method: this.defaultMethod}
        }
    }

    goBackInHistory() {
        /* Default behaviour: go back in history in the arva router */
        let previousRoute = this.getPreviousRoute();
        this.router.go(previousRoute.controller, previousRoute.method, previousRoute.parameters || null);
    }

    _setupNativeBackButtonListener() {
        this._backButtonEnabled = true;
        document.addEventListener("backbutton", (e) => {
            if (!this._backButtonEnabled) {
                e.preventDefault();
            } else {
                this.goBackInHistory();
            }
        }, false);
    }

    /**
     * Executes the controller handler associated with a given route, passing the route as a parameter.
     * @param {Object} rule Rule handler to execute.
     * @param {Object} route Route object to pass as parameter.
     * @returns {void}
     * @private
     */
    _executeRoute(rule, route) {
        /* Make the controller active for current scope */
        if (rule['enter'](route)) {
            this.emit('routechange', route);
        }
    }

    /**
     * Checks if the current route is already present in the history stack, and if so removes all entries after
     * and including the first occurrence. It will then append the current route to the history stack.
     * @param {Object} currentRoute Route object containing url, controller, method, keys, and values.
     * @returns {void}
     * @private
     */
    _setHistory(currentRoute) {
        for (let i = 0; i < this.history.length; i++) {
            let previousRoute = this.history[i];
            if (currentRoute.controller === previousRoute.controller &&
                currentRoute.method === previousRoute.method &&
                isEqual(currentRoute.values, previousRoute.values)) {
                this.history.splice(i, this.history.length - i);
                break;
            }
        }

        this.history.push(currentRoute);
    }

    /**
     * CheckS whether a route is already present in the history stack.
     * @param {Object} currentRoute Route object containing url, controller, method, keys, and values.
     * @returns {Boolean} Whether the route has been visited previously.
     * @private
     */
    _hasVisited(currentRoute) {
        for (let i = 0; i < this.history.length; i++) {
            let previousRoute = this.history[i];
            if (currentRoute.controller === previousRoute.controller &&
                currentRoute.method === previousRoute.method &&
                isEqual(currentRoute.values, previousRoute.values)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the Famous-Flex animation spec for two given routes. Takes its spec inputs from the specs set in
     * router.setControllerSpecs(), which is called from the app constructor.
     * @param {Object} previousRoute Previous route object containing url, controller, method, keys, and values.
     * @param {Object} currentRoute Current route object containing url, controller, method, keys, and values.
     * @returns {Object} A spec object if one is found, or an empty object otherwise.
     * @private
     */
    _getAnimationSpec(previousRoute, currentRoute) {
        let fromController = previousRoute.controller;
        let toController = currentRoute.controller;

        if (fromController.indexOf('Controller') === -1) {
            fromController += 'Controller';
        }
        if (toController.indexOf('Controller') === -1) {
            toController += 'Controller';
        }

        /* We're on exactly the same page as before */
        if (currentRoute.controller === previousRoute.controller &&
            currentRoute.method === previousRoute.method &&
            isEqual(currentRoute.values, previousRoute.values)) {
            return {};
        }

        /* Same controller, different method or different parameters */
        if (currentRoute.controller === previousRoute.controller) {

            let direction = this._hasVisited(currentRoute) ? 'previous' : 'next';
            if (this.specs && this.specs[fromController] && this.specs[fromController].methods) {
                return this.specs[fromController].methods[direction];
            }

            /* Default method-to-method animations, used only if not overridden in app's controllers spec. */
            let defaults = {
                'previous': {
                    transition: {duration: 400, curve: Easing.outBack},
                    animation: AnimationController.Animation.Slide.Right
                },
                'next': {
                    transition: {duration: 400, curve: Easing.outBack},
                    animation: AnimationController.Animation.Slide.Left
                }
            };
            return defaults[direction];
        }

        /* Different controller */
        if (this.specs && this.specs.hasOwnProperty(toController) && this.specs[toController].controllers) {
            let controllerSpecs = this.specs[toController].controllers;
            for (let specIndex in controllerSpecs) {
                let spec = controllerSpecs[specIndex];
                if (spec.activeFrom && spec.activeFrom.indexOf(fromController) !== -1) {
                    return spec;
                }
            }
        }

        console.log('No spec defined from ' + fromController + ' to ' + toController + '. Please check router.setControllerSpecs() in your app constructor.');
    }

    /**
     * Extracts a controller name from a given string, constructor, or controller instance. 'Controller' part is not included in the returned name.
     * E.g. _getControllerName(HomeController) -> 'Home'.
     * @param {Function|Object|String} controller String, constructor, or controller instance.
     * @returns {String} Name of the controller
     * @private
     */
    _getControllerName(controller) {
        if (typeof controller === 'string') {
            return controller.replace('Controller', '');
        } else if (typeof controller === 'function' && Object.getPrototypeOf(controller).constructor.name == 'Function') {
            /* The _name property is set by babel-plugin-transform-runtime-constructor-name.
             * This is done so Controller class names remain available in minimised code. */
            let controllerName = controller._name || controller.name;
            return controllerName.replace('Controller', '');
        } else {
            return typeof controller === 'object' ?
                Object.getPrototypeOf(controller).constructor.name.replace('Controller', '') : typeof controller;
        }
    }

}<commit_msg>Fixed faulty go function call in ArvaRouter<commit_after>/**

 @author: Hans van den Akker (mysim1)
 @license NPOSL-3.0
 @copyright Bizboard, 2015

 */

import isEqual                      from 'lodash/isEqual';
import {Router}                     from '../core/Router.js';
import {provide}                    from '../utils/di/Decorators.js';
import Easing                       from 'famous/transitions/Easing.js';
import AnimationController          from 'famous-flex/AnimationController.js';

/**
 * Emits the event 'routechange' with {url,controller,controllerObject,method,keys,values} when the route has changed
 */
@provide(Router)
export class ArvaRouter extends Router {

    routes = {};
    history = [];
    decode = decodeURIComponent;
    defaultController = 'Home';
    defaultMethod = 'Index';

    constructor() {
        super();
        if (window == null) {
            return;
        }
        window.addEventListener('hashchange', this.run);

        if (window == null) {
            return;
        }

        this.routes = {};
        this.history = [];
        this.decode = decodeURIComponent;

        window.addEventListener('hashchange', this.run);
        this._setupNativeBackButtonListener();
    }

    /**
     * Sets the initial controller and method to be activated whenever the controllers are activated.
     * @param {Controller|Function|String} controller Default controller instance, controller constructor, or controller name to go to.
     * @param {String} method Default method to call in given controller.
     * @returns {void}
     */
    setDefault(controller, method = null) {

        this.defaultController = this._getControllerName(controller);

        if (method != null) {
            this.defaultMethod = method;
        }
    }

    /**
     * Sets the animation specs object for use by the famous-flex AnimationController.
     * @param {Object} specs Animation specs, keyed by target controller.
     * @returns {void}
     */
    setControllerSpecs(specs) {
        this.specs = specs;
    }

    /**
     * Triggers navigation to one of the controllers
     * @param {Controller|Function|String} controller The controller instance, controller constructor, or controller name to go to.
     * @param {String} method The method to call in given controller.
     * @param {Object} params Dictonary of key-value pairs containing named arguments (i.e. {id: 1, test: "yes"})
     * @returns {void}
     */
    go(controller, method, params = null) {

        let controllerName = this._getControllerName(controller);

        let routeRoot = controllerName
            .replace('Controller', '');

        //TODO Can we skip this code?
        if(routeRoot === this.defaultController){
            routeRoot = '';
        }

        let hash = '#' + (routeRoot.length > 0 ? '/' + routeRoot : '') + ('/' + method);
        if (params !== null) {
            for (let i = 0; i < Object.keys(params).length; i++) {
                var key = Object.keys(params)[i];
                hash += i == 0 ? '?' : '&';
                hash += (key + '=' + params[key]);
            }
        }

        if (history.pushState) {
            history.pushState(null, null, hash);
        }

        this.run();
    }

    /**
     * Registers a single controller.
     * @param {String} route Route to trigger handler on.
     * @param {Object} handlers
     * @param {Function} handler.enter Method to call on entering a route.
     * @param {Function} handler.leave Method to call on when leaving a route.
     * @returns {void}
     */
    add(route, {enter, leave}, controller) {
        let pieces = route.split('/'),
            rules = this.routes;

        for (let i = 0; i < pieces.length; ++i) {
            let piece = pieces[i],
                name = piece[0] === ':' ? ':' : piece;

            rules = rules[name] || (rules[name] = {});

            if (name === ':') {
                rules['@name'] = piece.slice(1);
            }
        }

        rules['enter'] = enter;
        rules['leave'] = leave;
        rules['controller'] = controller;

    }

    /**
     * On a route change, calls the corresponding controller method with the given parameter values.
     * @returns {Boolean} Whether the current route was successfully ran.
     */
    run() {
        let url = window.location.hash.replace('#', '');

        if (url !== '') {
            url = url.replace('/?', '?');
            url[0] === '/' && (url = url.slice(1));
            url.slice(-1) === '/' && (url = url.slice(0, -1));
        }

        let rules = this.routes,
            querySplit = url.split('?'),
            pieces = querySplit[0].split('/'),
            values = [],
            keys = [],
            method = '';
        for (let piece in pieces) {
            if (pieces[piece].indexOf('=') > -1) {
                let splitted = pieces[piece].split('=');
                pieces[piece] = splitted[0];
                querySplit.push(pieces[piece] + '=' + splitted[1]);
            }
        }

        let rule = null;
        let controller;

        // if there is no controller reference, assume we have hit the default Controller
        if (pieces.length === 1 && pieces[0].length === 0) {
            pieces[0] = this.defaultController;
            pieces.push(this.defaultMethod);
        } else if (pieces.length === 1 && pieces[0].length > 0) {
            pieces.unshift(this.defaultController);
        }

        controller = pieces[0];

        // Parse the non-query portion of the URL...
        for (let i = 0; i < pieces.length && rules; ++i) {
            let piece = this.decode(pieces[i]);
            rule = rules[piece];

            if (!rule && (rule = rules[':'])) {
                method = piece;
            }

            rules = rules[piece];
        }

        (function parseQuery(q) {
            let query = q.split('&');

            for (let i = 0; i < query.length; ++i) {
                let nameValue = query[i].split('=');

                if (nameValue.length > 1) {
                    keys.push(nameValue[0]);
                    values.push(this.decode(nameValue[1]));
                }
            }
        }).call(this, querySplit.length > 1 ? querySplit[1] : '');

        if (rule && rule['enter']) {

            /* Push current route to the history stack for later use */
            let previousRoute = this.history.length ? this.history[this.history.length - 1] : undefined;
            let currentRoute = {
                url: url,
                controller: controller,
                controllerObject: rule['controller'],
                method: method,
                keys: keys,
                values: values
            };

            if(previousRoute){
                if(currentRoute.controllerObject !== previousRoute.controllerObject){
                    this.routes[previousRoute.controller][':']['leave'](currentRoute);
                }
            }
            currentRoute.spec = previousRoute ? this._getAnimationSpec(previousRoute, currentRoute) : (this._initialSpec || {});
            this._setHistory(currentRoute);

            this._executeRoute(rule, currentRoute);

            return true;
        } else {
            console.log('Controller doesn\'t exist!');
        }

        return false;
    }

    setInitialSpec(spec) {
        this._initialSpec = spec;
    }

    setBackButtonEnabled(enabled) {
        this._backButtonEnabled = enabled;
    }

    isBackButtonEnabled() {
        return this._backButtonEnabled;
    }

    /**
     * Return the previous known route, or default route if no route stack is present
     * @returns {*}
     */
    getPreviousRoute(){
        let {history} = this;
        if (history.length > 1) {
            let {controller, method, keys, values} = history[history.length - 2];
            let inputObject = {};
            for (let i = 0; i < keys.length; i++) {
                inputObject[keys[i]] = values[i];
            }
           return {controller: controller, method: method, parameters: inputObject};
        } else {
            return {controller: this.defaultController, method: this.defaultMethod}
        }
    }

    goBackInHistory() {
        /* Default behaviour: go back in history in the arva router */
        let previousRoute = this.getPreviousRoute();
        this.go(previousRoute.controller, previousRoute.method, previousRoute.parameters || null);
    }

    _setupNativeBackButtonListener() {
        this._backButtonEnabled = true;
        document.addEventListener("backbutton", (e) => {
            if (!this._backButtonEnabled) {
                e.preventDefault();
            } else {
                this.goBackInHistory();
            }
        }, false);
    }

    /**
     * Executes the controller handler associated with a given route, passing the route as a parameter.
     * @param {Object} rule Rule handler to execute.
     * @param {Object} route Route object to pass as parameter.
     * @returns {void}
     * @private
     */
    _executeRoute(rule, route) {
        /* Make the controller active for current scope */
        if (rule['enter'](route)) {
            this.emit('routechange', route);
        }
    }

    /**
     * Checks if the current route is already present in the history stack, and if so removes all entries after
     * and including the first occurrence. It will then append the current route to the history stack.
     * @param {Object} currentRoute Route object containing url, controller, method, keys, and values.
     * @returns {void}
     * @private
     */
    _setHistory(currentRoute) {
        for (let i = 0; i < this.history.length; i++) {
            let previousRoute = this.history[i];
            if (currentRoute.controller === previousRoute.controller &&
                currentRoute.method === previousRoute.method &&
                isEqual(currentRoute.values, previousRoute.values)) {
                this.history.splice(i, this.history.length - i);
                break;
            }
        }

        this.history.push(currentRoute);
    }

    /**
     * CheckS whether a route is already present in the history stack.
     * @param {Object} currentRoute Route object containing url, controller, method, keys, and values.
     * @returns {Boolean} Whether the route has been visited previously.
     * @private
     */
    _hasVisited(currentRoute) {
        for (let i = 0; i < this.history.length; i++) {
            let previousRoute = this.history[i];
            if (currentRoute.controller === previousRoute.controller &&
                currentRoute.method === previousRoute.method &&
                isEqual(currentRoute.values, previousRoute.values)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the Famous-Flex animation spec for two given routes. Takes its spec inputs from the specs set in
     * router.setControllerSpecs(), which is called from the app constructor.
     * @param {Object} previousRoute Previous route object containing url, controller, method, keys, and values.
     * @param {Object} currentRoute Current route object containing url, controller, method, keys, and values.
     * @returns {Object} A spec object if one is found, or an empty object otherwise.
     * @private
     */
    _getAnimationSpec(previousRoute, currentRoute) {
        let fromController = previousRoute.controller;
        let toController = currentRoute.controller;

        if (fromController.indexOf('Controller') === -1) {
            fromController += 'Controller';
        }
        if (toController.indexOf('Controller') === -1) {
            toController += 'Controller';
        }

        /* We're on exactly the same page as before */
        if (currentRoute.controller === previousRoute.controller &&
            currentRoute.method === previousRoute.method &&
            isEqual(currentRoute.values, previousRoute.values)) {
            return {};
        }

        /* Same controller, different method or different parameters */
        if (currentRoute.controller === previousRoute.controller) {

            let direction = this._hasVisited(currentRoute) ? 'previous' : 'next';
            if (this.specs && this.specs[fromController] && this.specs[fromController].methods) {
                return this.specs[fromController].methods[direction];
            }

            /* Default method-to-method animations, used only if not overridden in app's controllers spec. */
            let defaults = {
                'previous': {
                    transition: {duration: 400, curve: Easing.outBack},
                    animation: AnimationController.Animation.Slide.Right
                },
                'next': {
                    transition: {duration: 400, curve: Easing.outBack},
                    animation: AnimationController.Animation.Slide.Left
                }
            };
            return defaults[direction];
        }

        /* Different controller */
        if (this.specs && this.specs.hasOwnProperty(toController) && this.specs[toController].controllers) {
            let controllerSpecs = this.specs[toController].controllers;
            for (let specIndex in controllerSpecs) {
                let spec = controllerSpecs[specIndex];
                if (spec.activeFrom && spec.activeFrom.indexOf(fromController) !== -1) {
                    return spec;
                }
            }
        }

        console.log('No spec defined from ' + fromController + ' to ' + toController + '. Please check router.setControllerSpecs() in your app constructor.');
    }

    /**
     * Extracts a controller name from a given string, constructor, or controller instance. 'Controller' part is not included in the returned name.
     * E.g. _getControllerName(HomeController) -> 'Home'.
     * @param {Function|Object|String} controller String, constructor, or controller instance.
     * @returns {String} Name of the controller
     * @private
     */
    _getControllerName(controller) {
        if (typeof controller === 'string') {
            return controller.replace('Controller', '');
        } else if (typeof controller === 'function' && Object.getPrototypeOf(controller).constructor.name == 'Function') {
            /* The _name property is set by babel-plugin-transform-runtime-constructor-name.
             * This is done so Controller class names remain available in minimised code. */
            let controllerName = controller._name || controller.name;
            return controllerName.replace('Controller', '');
        } else {
            return typeof controller === 'object' ?
                Object.getPrototypeOf(controller).constructor.name.replace('Controller', '') : typeof controller;
        }
    }

}<|endoftext|>


<commit_before>import {
  devices
, hiddenFromAT
} from '../util'

// test will be run in order
export default [{
    tagName: 'img'
  , msg: 'The img does not have an `alt` prop, screen-readers will not know what it is'
  , url: 'https://dev.w3.org/html5/alt-techniques'
  , test (tagName, props) {
      const hidden = hiddenFromAT(props)
      const alt    = typeof props.alt !== 'string'

      return !hidden && !alt
    }
  , affects: [
      devices.screenReaders
    ]
  }
, {
    tagName: 'img'
  , msg: 'The `alt` prop cannot be empty string if role="presentation" is not set.'
  , url: 'https://www.w3.org/TR/wai-aria/roles#presentation'
  , test (tagName, props) {
      const hidden = hiddenFromAT(props)
      const alt    = typeof props.alt === 'string'
      const empty  = alt && props.alt === ''
      const pres   = props.role === 'presentation'

      return !hidden && alt && empty && pres
    }
  , affects: [
      devices.screenReaders
    ]
}]

export const pass = [
  {
    when: 'the img has an `alt`'
  , render: React =>
  }
, {
    when: 'the img has an empty `alt` and role="presentation"'
  , render: React =>
  }
, {
    when: 'the img is aria-hidden'
  , render: React =>
  }
]

export const fail = [
  {
    when: 'the img doen\'t have an `alt`'
  , render: React =>
  }
, {
    when: 'the img has alt="" but no role="presentation"'
  , render: React =>
  }
]

export const description = `
Enforce that an \`img\` element contains the \`alt\` prop. The \`alt\` attribute specifies
an alternate text for an image, if the image cannot be displayed.
`
<commit_msg>fix false positives<commit_after>import {
  devices
, hiddenFromAT
, hasProp
} from '../util'

// test will be run in order
export default [{
    tagName: 'img'
  , msg: 'The img does not have an `alt` prop, screen-readers will not know what it is'
  , url: 'https://dev.w3.org/html5/alt-techniques'
  , test (tagName, props) {
      const hidden = hiddenFromAT(props)
      const alt    = hasProp(props, 'alt')

      return hidden || alt
    }
  , affects: [
      devices.screenReaders
    ]
  }
, {
    tagName: 'img'
  , msg: 'The `alt` prop cannot be empty string if role="presentation" is not set.'
  , url: 'https://www.w3.org/TR/wai-aria/roles#presentation'
  , test (tagName, props) {
      const hidden = hiddenFromAT(props)
      const empty  = props.alt === ''
      const pres   = props.role === 'presentation'

      return hidden || !empty || pres
    }
  , affects: [
      devices.screenReaders
    ]
}]

export const pass = [
  {
    when: 'the img has an `alt`'
  , render: React =>
  }
, {
    when: 'the img has an empty `alt` and role="presentation"'
  , render: React =>
  }
, {
    when: 'the img is aria-hidden'
  , render: React =>
  }
]

export const fail = [
  {
    when: 'the img doen\'t have an `alt`'
  , render: React =>
  }
, {
    when: 'the img has alt="" but no role="presentation"'
  , render: React =>
  }
]

export const description = `
Enforce that an \`img\` element contains the \`alt\` prop. The \`alt\` attribute specifies
an alternate text for an image, if the image cannot be displayed.
`
<|endoftext|>


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

import {PaymentsAsyncClient} from '../../third_party/gpay/src/payjs_async';
import {bytesToString, stringToBytes} from '../utils/bytes';
import {createCancelError} from '../utils/errors';
import {feCached} from './services';

const REDIRECT_STORAGE_KEY = 'subscribe.google.com:rk';

/**
 * @typedef {{
 *   forceRedirect: (boolean|undefined),
 * }}
 */
export let PayOptionsDef;

/**
 * @const {!Object<string, string>}
 * @package Visible for testing only.
 */
export const PAY_ORIGIN = {
  'PRODUCTION': 'https://pay.google.com',
  'SANDBOX': 'https://pay.sandbox.google.com',
};

/** @return {string} */
function payUrl() {
  return feCached(PAY_ORIGIN['$payEnvironment$'] + '/gp/p/ui/pay');
}

/**
 */
export class PayClient {
  /**
   * @param {!./deps.DepsDef} deps
   */
  constructor(deps) {
    /** @private @const {!Window} */
    this.win_ = deps.win();

    /** @private @const {!../components/activities.ActivityPorts} */
    this.activityPorts_ = deps.activities();

    /** @private {?function(!Promise<!Object>)} */
    this.responseCallback_ = null;

    /** @private {?Object} */
    this.request_ = null;

    /** @private {?Promise<!Object>} */
    this.response_ = null;

    /** @private @const {!./analytics-service.AnalyticsService} */
    this.analytics_ = deps.analytics();

    /** @private @const {!RedirectVerifierHelper} */
    this.redirectVerifierHelper_ = new RedirectVerifierHelper(this.win_);

    // Prepare new verifier pair.
    this.redirectVerifierHelper_.prepare();
  }

  /**
   * @param {!Object} options
   * @param {string} googleTransactionId
   * @param {function(!Promise<!Object>)} handler
   * @return {!PaymentsAsyncClient}
   * @private
   */
  createClient_(options, googleTransactionId, handler) {
    // Assign Google Transaction ID to PaymentsAsyncClient.googleTransactionId_
    // so it can be passed to gpay_async.js and stored in payment clearcut log.
    PaymentsAsyncClient.googleTransactionId_ = googleTransactionId;
    return new PaymentsAsyncClient(
      options,
      handler,
      /* useIframe */ false,
      this.activityPorts_.getOriginalWebActivityPorts()
    );
  }

  /**
   * @param {!../utils/preconnect.Preconnect} pre
   */
  preconnect(pre) {
    pre.prefetch(payUrl());
    pre.prefetch(
      'https://payments.google.com/payments/v4/js/integrator.js?ss=md'
    );
    pre.prefetch('https://clients2.google.com/gr/gr_full_2.0.6.js');
    pre.preconnect('https://www.gstatic.com/');
    pre.preconnect('https://fonts.googleapis.com/');
    pre.preconnect('https://www.google.com/');
  }

  /**
   * @return {string}
   */
  getType() {
    // TODO(alin04): remove once all references removed.
    return 'PAYJS';
  }

  /**
   * @param {!Object} paymentRequest
   * @param {!PayOptionsDef=} options
   */
  start(paymentRequest, options = {}) {
    this.request_ = paymentRequest;

    const client = this.createClient_(
      {
        environment: '$payEnvironment$',
        'i': {
          'redirectKey': this.redirectVerifierHelper_.restoreKey(),
        },
      },
      this.analytics_.getTransactionId(),
      this.handleResponse_.bind(this)
    );

    if (options.forceRedirect) {
      paymentRequest = Object.assign(paymentRequest, {
        'forceRedirect': options.forceRedirect || false,
      });
    }
    setInternalParam(
      paymentRequest,
      'disableNative',
      // The page cannot be iframed at this time. May be relaxed later
      // for AMP and similar contexts.
      this.win_ != this.top_()
    );
    let resolver = null;
    const promise = new Promise(resolve => (resolver = resolve));
    // Notice that the callback for verifier may execute asynchronously.
    this.redirectVerifierHelper_.useVerifier(verifier => {
      if (verifier) {
        setInternalParam(paymentRequest, 'redirectVerifier', verifier);
      }
      if (options.forceRedirect) {
        return this.analytics_.getLoggingPromise().then(() => {
          client.loadPaymentData(paymentRequest);
          resolver(true);
        });
      } else {
        client.loadPaymentData(paymentRequest);
        resolver(true);
      }
    });
    return promise;
  }

  /**
   * @param {function(!Promise<!Object>)} callback
   */
  onResponse(callback) {
    this.responseCallback_ = callback;
    const response = this.response_;
    if (response) {
      Promise.resolve().then(() => {
        if (response) {
          callback(this.convertResponse_(response, this.request_));
        }
      });
    }
  }

  /**
   * @param {!Promise<!Object>} responsePromise
   * @private
   */
  handleResponse_(responsePromise) {
    this.response_ = responsePromise;
    if (this.responseCallback_) {
      this.responseCallback_(
        this.convertResponse_(this.response_, this.request_)
      );
    }
  }

  /**
   * @param {!Promise<!Object>} response
   * @param {?Object} request
   * @return {!Promise<!Object>}
   * @private
   */
  convertResponse_(response, request) {
    return response
      .then(
        // Temporary client side solution to remember the
        // input params. TODO: Remove this once server-side
        // input preservation is done and is part of the response.
        res => {
          if (request) {
            res['paymentRequest'] = request;
          }
          return res;
        }
      )
      .catch(reason => {
        if (typeof reason == 'object' && reason['statusCode'] == 'CANCELED') {
          return Promise.reject(createCancelError(this.win_));
        }
        return Promise.reject(reason);
      });
  }

  /**
   * @return {!Window}
   * @private
   */
  top_() {
    // Only exists for testing since it's not possible to override `window.top`.
    return this.win_.top;
  }
}

/**
 * @typedef {{
 *   key: string,
 *   verifier: string,
 * }}
 */
let RedirectVerifierPairDef;

/**
 * This helper generates key/verifier pair for the redirect mode. When the
 * redirect mode is used, the encrypted payload is returned via nivigation URL.
 * This payload need to be decrypted and to avoid session fixation attacks, a
 * verifier has to be used. This redirect verifier is not the only session
 * verifier in use: we also use GAIA. However, we have to fallback to this
 * verifier when GAIA is not available.
 *
 * @package Visible for testing only.
 */
export class RedirectVerifierHelper {
  /**
   * @param {!Window} win
   */
  constructor(win) {
    /** @private @const {!Window} */
    this.win_ = win;

    /** @private {boolean} */
    this.pairCreated_ = false;

    /** @private {?RedirectVerifierPairDef} */
    this.pair_ = null;

    /** @private {?Promise<?RedirectVerifierPairDef>} */
    this.pairPromise_ = null;
  }

  /**
   * To avoid popup blockers, the key/verifier pair is created as soon as
   * possible.
   * @return {?Promise}
   */
  prepare() {
    return this.getOrCreatePair_(() => {});
  }

  /**
   * Calls the provided callback with the generated redirect verifier. This
   * API is sync/async, which is a big anti-pattern. However, it's necessary
   * to reduce the risk of popup blockers. If the verifier is already available
   * (see `prepare` method), the callback will be called immediately and thus
   * in the same event loop as the user action.
   *
   * The return verifier could be `null`. This could mean either that its
   * generation failed, or if the platform doesn't support necessary APIs, such
   * as Web Crypto. The redirect can still proceed and try to fallback on GAIA
   * as a redirect verifier. The set of platforms where GAIA is not available
   * and the redirect verifier cannot be created is negligible.
   *
   * The key corresponding to the returned verifier is stored in the session
   * storage and can be later restored using `restoreKey` method.
   *
   * @param {function(?string)} callback
   */
  useVerifier(callback) {
    this.getOrCreatePair_(pair => {
      if (pair) {
        try {
          this.win_.localStorage.setItem(REDIRECT_STORAGE_KEY, pair.key);
        } catch (e) {
          // If storage has failed, there's no point in using the verifer.
          // However, there are other ways to recover the redirect, so it's
          // not necessarily a fatal condition.
          pair = null;
        }
      }
      callback((pair && pair.verifier) || null);
    });
  }

  /**
   * Restores the redirect key from the session storage. The key may be null.
   * @return {?string}
   */
  restoreKey() {
    try {
      return (
        (this.win_.localStorage &&
          this.win_.localStorage.getItem(REDIRECT_STORAGE_KEY)) ||
        null
      );
    } catch (e) {
      return null;
    }
  }

  /**
   * @param {function(?RedirectVerifierPairDef)} callback
   * @return {?Promise}
   * @private
   */
  getOrCreatePair_(callback) {
    this.createPair_();
    if (this.pairCreated_) {
      // Already created.
      callback(this.pair_);
    } else if (this.pairPromise_) {
      // Otherwise wait for it to be created.
      this.pairPromise_.then(pair => callback(pair));
    }
    return this.pairPromise_;
  }

  /**
   * @private
   */
  createPair_() {
    // Either already created or already started.
    if (this.pairCreated_ || this.pairPromise_) {
      return;
    }

    // Check that the platform can fully support verification. That means
    // that it's expected to implement the following APIs:
    // a. Local storage (localStorage);
    // b. WebCrypto (crypto.subtle);
    // c. Crypto random (crypto.getRandomValues);
    // d. SHA284 (crypto.subtle.digest).
    const crypto = this.win_.crypto;
    if (
      this.win_.localStorage &&
      crypto &&
      crypto.getRandomValues &&
      crypto.subtle &&
      crypto.subtle.digest
    ) {
      this.pairPromise_ = new Promise((resolve, reject) => {
        // 1. Use crypto random to create a 128-bit (16 byte) redirect key.
        const keyBytes = new Uint8Array(16);
        crypto.getRandomValues(keyBytes);

        // 2. Encode key as base64.
        const key = btoa(bytesToString(keyBytes));

        // 3. Create a hash.
        crypto.subtle.digest({name: 'SHA-384'}, stringToBytes(key)).then(
          buffer => {
            const verifier = btoa(
              bytesToString(
                new Uint8Array(/** @type {!ArrayBuffer} */ (buffer))
              )
            );
            resolve({key, verifier});
          },
          reason => {
            reject(reason);
          }
        );
      })
        .catch(() => {
          // Ignore failures. A failure to create a redirect verifier is often
          // recoverable.
          return null;
        })
        .then(pair => {
          this.pairCreated_ = true;
          this.pair_ = pair;
          return pair;
        });
    } else {
      // Not supported.
      this.pairCreated_ = true;
      this.pair_ = null;
    }
  }
}

/**
 * @param {!Object} paymentRequest
 * @param {string} param
 * @param {*} value
 */
function setInternalParam(paymentRequest, param, value) {
  paymentRequest['i'] = Object.assign(paymentRequest['i'] || {}, {
    [param]: value,
  });
}
<commit_msg>Put PaymentsAsyncClient instantiation back in ctor.<commit_after>/**
 * Copyright 2018 The Subscribe with Google Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {PaymentsAsyncClient} from '../../third_party/gpay/src/payjs_async';
import {bytesToString, stringToBytes} from '../utils/bytes';
import {createCancelError} from '../utils/errors';
import {feCached} from './services';

const REDIRECT_STORAGE_KEY = 'subscribe.google.com:rk';

/**
 * @typedef {{
 *   forceRedirect: (boolean|undefined),
 * }}
 */
export let PayOptionsDef;

/**
 * @const {!Object<string, string>}
 * @package Visible for testing only.
 */
export const PAY_ORIGIN = {
  'PRODUCTION': 'https://pay.google.com',
  'SANDBOX': 'https://pay.sandbox.google.com',
};

/** @return {string} */
function payUrl() {
  return feCached(PAY_ORIGIN['$payEnvironment$'] + '/gp/p/ui/pay');
}

/**
 */
export class PayClient {
  /**
   * @param {!./deps.DepsDef} deps
   */
  constructor(deps) {
    /** @private @const {!Window} */
    this.win_ = deps.win();

    /** @private @const {!../components/activities.ActivityPorts} */
    this.activityPorts_ = deps.activities();

    /** @private {?function(!Promise<!Object>)} */
    this.responseCallback_ = null;

    /** @private {?Object} */
    this.request_ = null;

    /** @private {?Promise<!Object>} */
    this.response_ = null;

    /** @private @const {!./analytics-service.AnalyticsService} */
    this.analytics_ = deps.analytics();

    /** @private @const {!RedirectVerifierHelper} */
    this.redirectVerifierHelper_ = new RedirectVerifierHelper(this.win_);

    /** @private @const {!PaymentsAsyncClient} */
    this.client_ = this.createClient_(
      {
        environment: '$payEnvironment$',
        'i': {
          'redirectKey': this.redirectVerifierHelper_.restoreKey(),
        },
      },
      this.analytics_.getTransactionId(),
      this.handleResponse_.bind(this)
    );

    // Prepare new verifier pair.
    this.redirectVerifierHelper_.prepare();
  }

  /**
   * @param {!Object} options
   * @param {string} googleTransactionId
   * @param {function(!Promise<!Object>)} handler
   * @return {!PaymentsAsyncClient}
   * @private
   */
  createClient_(options, googleTransactionId, handler) {
    // Assign Google Transaction ID to PaymentsAsyncClient.googleTransactionId_
    // so it can be passed to gpay_async.js and stored in payment clearcut log.
    PaymentsAsyncClient.googleTransactionId_ = googleTransactionId;
    return new PaymentsAsyncClient(
      options,
      handler,
      /* useIframe */ false,
      this.activityPorts_.getOriginalWebActivityPorts()
    );
  }

  /**
   * @param {!../utils/preconnect.Preconnect} pre
   */
  preconnect(pre) {
    pre.prefetch(payUrl());
    pre.prefetch(
      'https://payments.google.com/payments/v4/js/integrator.js?ss=md'
    );
    pre.prefetch('https://clients2.google.com/gr/gr_full_2.0.6.js');
    pre.preconnect('https://www.gstatic.com/');
    pre.preconnect('https://fonts.googleapis.com/');
    pre.preconnect('https://www.google.com/');
  }

  /**
   * @return {string}
   */
  getType() {
    // TODO(alin04): remove once all references removed.
    return 'PAYJS';
  }

  /**
   * @param {!Object} paymentRequest
   * @param {!PayOptionsDef=} options
   */
  start(paymentRequest, options = {}) {
    this.request_ = paymentRequest;

    if (options.forceRedirect) {
      paymentRequest = Object.assign(paymentRequest, {
        'forceRedirect': options.forceRedirect || false,
      });
    }
    setInternalParam(
      paymentRequest,
      'disableNative',
      // The page cannot be iframed at this time. May be relaxed later
      // for AMP and similar contexts.
      this.win_ != this.top_()
    );
    let resolver = null;
    const promise = new Promise(resolve => (resolver = resolve));
    // Notice that the callback for verifier may execute asynchronously.
    this.redirectVerifierHelper_.useVerifier(verifier => {
      if (verifier) {
        setInternalParam(paymentRequest, 'redirectVerifier', verifier);
      }
      if (options.forceRedirect) {
        const client = this.client_;
        return this.analytics_.getLoggingPromise().then(() => {
          client.loadPaymentData(paymentRequest);
          resolver(true);
        });
      } else {
        this.client_.loadPaymentData(paymentRequest);
        resolver(true);
      }
    });
    return promise;
  }

  /**
   * @param {function(!Promise<!Object>)} callback
   */
  onResponse(callback) {
    this.responseCallback_ = callback;
    const response = this.response_;
    if (response) {
      Promise.resolve().then(() => {
        if (response) {
          callback(this.convertResponse_(response, this.request_));
        }
      });
    }
  }

  /**
   * @param {!Promise<!Object>} responsePromise
   * @private
   */
  handleResponse_(responsePromise) {
    this.response_ = responsePromise;
    if (this.responseCallback_) {
      this.responseCallback_(
        this.convertResponse_(this.response_, this.request_)
      );
    }
  }

  /**
   * @param {!Promise<!Object>} response
   * @param {?Object} request
   * @return {!Promise<!Object>}
   * @private
   */
  convertResponse_(response, request) {
    return response
      .then(
        // Temporary client side solution to remember the
        // input params. TODO: Remove this once server-side
        // input preservation is done and is part of the response.
        res => {
          if (request) {
            res['paymentRequest'] = request;
          }
          return res;
        }
      )
      .catch(reason => {
        if (typeof reason == 'object' && reason['statusCode'] == 'CANCELED') {
          return Promise.reject(createCancelError(this.win_));
        }
        return Promise.reject(reason);
      });
  }

  /**
   * @return {!Window}
   * @private
   */
  top_() {
    // Only exists for testing since it's not possible to override `window.top`.
    return this.win_.top;
  }
}

/**
 * @typedef {{
 *   key: string,
 *   verifier: string,
 * }}
 */
let RedirectVerifierPairDef;

/**
 * This helper generates key/verifier pair for the redirect mode. When the
 * redirect mode is used, the encrypted payload is returned via nivigation URL.
 * This payload need to be decrypted and to avoid session fixation attacks, a
 * verifier has to be used. This redirect verifier is not the only session
 * verifier in use: we also use GAIA. However, we have to fallback to this
 * verifier when GAIA is not available.
 *
 * @package Visible for testing only.
 */
export class RedirectVerifierHelper {
  /**
   * @param {!Window} win
   */
  constructor(win) {
    /** @private @const {!Window} */
    this.win_ = win;

    /** @private {boolean} */
    this.pairCreated_ = false;

    /** @private {?RedirectVerifierPairDef} */
    this.pair_ = null;

    /** @private {?Promise<?RedirectVerifierPairDef>} */
    this.pairPromise_ = null;
  }

  /**
   * To avoid popup blockers, the key/verifier pair is created as soon as
   * possible.
   * @return {?Promise}
   */
  prepare() {
    return this.getOrCreatePair_(() => {});
  }

  /**
   * Calls the provided callback with the generated redirect verifier. This
   * API is sync/async, which is a big anti-pattern. However, it's necessary
   * to reduce the risk of popup blockers. If the verifier is already available
   * (see `prepare` method), the callback will be called immediately and thus
   * in the same event loop as the user action.
   *
   * The return verifier could be `null`. This could mean either that its
   * generation failed, or if the platform doesn't support necessary APIs, such
   * as Web Crypto. The redirect can still proceed and try to fallback on GAIA
   * as a redirect verifier. The set of platforms where GAIA is not available
   * and the redirect verifier cannot be created is negligible.
   *
   * The key corresponding to the returned verifier is stored in the session
   * storage and can be later restored using `restoreKey` method.
   *
   * @param {function(?string)} callback
   */
  useVerifier(callback) {
    this.getOrCreatePair_(pair => {
      if (pair) {
        try {
          this.win_.localStorage.setItem(REDIRECT_STORAGE_KEY, pair.key);
        } catch (e) {
          // If storage has failed, there's no point in using the verifer.
          // However, there are other ways to recover the redirect, so it's
          // not necessarily a fatal condition.
          pair = null;
        }
      }
      callback((pair && pair.verifier) || null);
    });
  }

  /**
   * Restores the redirect key from the session storage. The key may be null.
   * @return {?string}
   */
  restoreKey() {
    try {
      return (
        (this.win_.localStorage &&
          this.win_.localStorage.getItem(REDIRECT_STORAGE_KEY)) ||
        null
      );
    } catch (e) {
      return null;
    }
  }

  /**
   * @param {function(?RedirectVerifierPairDef)} callback
   * @return {?Promise}
   * @private
   */
  getOrCreatePair_(callback) {
    this.createPair_();
    if (this.pairCreated_) {
      // Already created.
      callback(this.pair_);
    } else if (this.pairPromise_) {
      // Otherwise wait for it to be created.
      this.pairPromise_.then(pair => callback(pair));
    }
    return this.pairPromise_;
  }

  /**
   * @private
   */
  createPair_() {
    // Either already created or already started.
    if (this.pairCreated_ || this.pairPromise_) {
      return;
    }

    // Check that the platform can fully support verification. That means
    // that it's expected to implement the following APIs:
    // a. Local storage (localStorage);
    // b. WebCrypto (crypto.subtle);
    // c. Crypto random (crypto.getRandomValues);
    // d. SHA284 (crypto.subtle.digest).
    const crypto = this.win_.crypto;
    if (
      this.win_.localStorage &&
      crypto &&
      crypto.getRandomValues &&
      crypto.subtle &&
      crypto.subtle.digest
    ) {
      this.pairPromise_ = new Promise((resolve, reject) => {
        // 1. Use crypto random to create a 128-bit (16 byte) redirect key.
        const keyBytes = new Uint8Array(16);
        crypto.getRandomValues(keyBytes);

        // 2. Encode key as base64.
        const key = btoa(bytesToString(keyBytes));

        // 3. Create a hash.
        crypto.subtle.digest({name: 'SHA-384'}, stringToBytes(key)).then(
          buffer => {
            const verifier = btoa(
              bytesToString(
                new Uint8Array(/** @type {!ArrayBuffer} */ (buffer))
              )
            );
            resolve({key, verifier});
          },
          reason => {
            reject(reason);
          }
        );
      })
        .catch(() => {
          // Ignore failures. A failure to create a redirect verifier is often
          // recoverable.
          return null;
        })
        .then(pair => {
          this.pairCreated_ = true;
          this.pair_ = pair;
          return pair;
        });
    } else {
      // Not supported.
      this.pairCreated_ = true;
      this.pair_ = null;
    }
  }
}

/**
 * @param {!Object} paymentRequest
 * @param {string} param
 * @param {*} value
 */
function setInternalParam(paymentRequest, param, value) {
  paymentRequest['i'] = Object.assign(paymentRequest['i'] || {}, {
    [param]: value,
  });
}
<|endoftext|>


<commit_before>/**
 * Copyright (c) Tijs van der Storm <Centrum Wiskunde & Informatica>.
 * All rights reserved.
 *
 * This file is licensed under the BSD 2-Clause License, which accompanies this project
 * and is available under https://opensource.org/licenses/BSD-2-Clause.
 *
 * Contributors:
 *  - Tijs van der Storm - storm@cwi.nl - CWI
 */

function registerCodeMirror(salix) {

	var codeMirrors = {};

	function parseSimpleMode(mode) {

		var jsMode = {};

		for (var i = 0; i < mode.mode.states.length; i++) {
			var state = mode.mode.states[i]
			var name = state.state.name;
			jsMode[name] = [];
			for (var j = 0; j < state.state.rules.length; j++) {
				var rule = state.state.rules[i];
				var token = rule.rule.tokens.length > 1
					? rule.rule.tokens : rule.rule.tokens[0];
				jsMode[name].push({regex: new RegExp(rule.rule.regex),
						token: token});
			}
		}

		return jsMode;
	}

	salix.Decoders.codeMirrorChange = function (args) {
		return function (editor, change) {
			return {type: 'codeMirrorChange',
				fromLine: change.from.line, fromCol: change.from.ch,
				toLine: change.to.line, toCol: change.to.ch,
				text: change.text.join('\n'),
				removed: change.removed.join("\n")};
		};
	};

	salix.Decoders.cursorActivity = function (args) {
		return function (editor) {
			var position = editor.getCursor();
			var line = position.line;
			var token = editor.getTokenAt(position);
			return  {type: 'cursorActivity', line: line, start: token.start,
				end: token.end, string: token.string, tokenType: token.type};
		};
	};

	function codeMirror(attach, id, attrs, props, events, extra) {
		var cm = CodeMirror(function(elt) { attach(elt); }, {});

		codeMirrors[id] = cm;

		// for remove event.
		var myHandlers = {};

		if (extra && extra.mode) {
			var mode = parseSimpleMode(extra.mode);
			CodeMirror.defineSimpleMode(extra.mode.mode.name, mode);
		}

		for (var key in props) {
			// todo: this logic is shared with setProp
			if (props.hasOwnProperty(key)) {
				var val = props[key];

				if (key === 'value') {
					cm.getDoc().setValue(val);
				}
				if (key == 'width') {
					cm.setSize(val, null);
				}
				else if (key === 'height') {
					cm.setSize(null, val);
				}
				else if (key === 'style') {
					cm.getWrapperElement().style = val;
				}
				else if (key === 'simpleMode') {
					// do defineSimpleMode
				}
				else {
					cm.setOption(key, val);
				}
			}
		}

		for (var key in events) {
			// TODO: shared with setEvent
			if (events.hasOwnProperty(key)) {
				var handler = salix.getNativeHandler(events[key]);
				myHandlers[key] = handler;
				cm.on(key, handler);
			}
		}

		setTimeout(function() {
            cm.refresh();
        }, 100);

		function patch(edits, attach) {
			edits = edits || [];

			for (var i = 0; i < edits.length; i++) {
				var edit = edits[i];
				var type = salix.nodeType(edit);

				switch (type) {

				case 'replace':
					salix.build(edit[type].html, attach);
					break;

				case 'setProp':
					var key = edit[type].name;
					var val = edit[type].val;
					if (key === 'value') {
						// ignore
					}
					else if (key === 'width') {
						cm.setSize(val, null);
					}
					else if (key === 'height') {
						cm.setSize(null, val);
					}
					else if (key === 'style') {
						cm.getWrapperElement().style = val;
					}
					else {
						cm.setOption(key, val);
					}
					break;

				case 'setEvent':
					var key = edit[type].name;
					var handler = salix.getNativeHandler(edit[type].handler);
					myHandlers[key] = handler;
					cm.on(key, handler);
					break

				case 'removeProp':
					var key = edit[type].name;
					if (key === 'width' || key === 'height') {
						// doesn't actually revert it.
						cm.setSize(null, null);
					}
					else if (key === 'style') {
						cm.getWrapperElement().style = '';
					}
					else {
						cm.setOption(key, CodeMirror.defaults[key]);
					}
					break;

				case 'removeEvent':
					var key = edit[type].name
					cm.off(key, myHandlers[key]);
					delete myHandlers[key];
					break;

				default:
					throw 'unsupported edit: ' + JSON.stringify(edit);

				}
			}
		}

		var dom = cm.getWrapperElement();
		dom.salix_native = {patch: patch};
	}

	function doCommand(cmd) {
		// TODO
	}

	salix.registerNative('codeMirror', codeMirror);
};<commit_msg>j not i<commit_after>/**
 * Copyright (c) Tijs van der Storm <Centrum Wiskunde & Informatica>.
 * All rights reserved.
 *
 * This file is licensed under the BSD 2-Clause License, which accompanies this project
 * and is available under https://opensource.org/licenses/BSD-2-Clause.
 *
 * Contributors:
 *  - Tijs van der Storm - storm@cwi.nl - CWI
 */

function registerCodeMirror(salix) {

	var codeMirrors = {};

	function parseSimpleMode(mode) {

		var jsMode = {};

		for (var i = 0; i < mode.mode.states.length; i++) {
			var state = mode.mode.states[i]
			var name = state.state.name;
			jsMode[name] = [];
			for (var j = 0; j < state.state.rules.length; j++) {
				var rule = state.state.rules[j];
				var token = rule.rule.tokens.length > 1
					? rule.rule.tokens : rule.rule.tokens[0];
				jsMode[name].push({regex: new RegExp(rule.rule.regex),
						token: token});
			}
		}

		return jsMode;
	}

	salix.Decoders.codeMirrorChange = function (args) {
		return function (editor, change) {
			return {type: 'codeMirrorChange',
				fromLine: change.from.line, fromCol: change.from.ch,
				toLine: change.to.line, toCol: change.to.ch,
				text: change.text.join('\n'),
				removed: change.removed.join("\n")};
		};
	};

	salix.Decoders.cursorActivity = function (args) {
		return function (editor) {
			var position = editor.getCursor();
			var line = position.line;
			var token = editor.getTokenAt(position);
			return  {type: 'cursorActivity', line: line, start: token.start,
				end: token.end, string: token.string, tokenType: token.type};
		};
	};

	function codeMirror(attach, id, attrs, props, events, extra) {
		var cm = CodeMirror(function(elt) { attach(elt); }, {});

		codeMirrors[id] = cm;

		// for remove event.
		var myHandlers = {};

		if (extra && extra.mode) {
			var mode = parseSimpleMode(extra.mode);
			CodeMirror.defineSimpleMode(extra.mode.mode.name, mode);
		}

		for (var key in props) {
			// todo: this logic is shared with setProp
			if (props.hasOwnProperty(key)) {
				var val = props[key];

				if (key === 'value') {
					cm.getDoc().setValue(val);
				}
				if (key == 'width') {
					cm.setSize(val, null);
				}
				else if (key === 'height') {
					cm.setSize(null, val);
				}
				else if (key === 'style') {
					cm.getWrapperElement().style = val;
				}
				else if (key === 'simpleMode') {
					// do defineSimpleMode
				}
				else {
					cm.setOption(key, val);
				}
			}
		}

		for (var key in events) {
			// TODO: shared with setEvent
			if (events.hasOwnProperty(key)) {
				var handler = salix.getNativeHandler(events[key]);
				myHandlers[key] = handler;
				cm.on(key, handler);
			}
		}

		setTimeout(function() {
            cm.refresh();
        }, 100);

		function patch(edits, attach) {
			edits = edits || [];

			for (var i = 0; i < edits.length; i++) {
				var edit = edits[i];
				var type = salix.nodeType(edit);

				switch (type) {

				case 'replace':
					salix.build(edit[type].html, attach);
					break;

				case 'setProp':
					var key = edit[type].name;
					var val = edit[type].val;
					if (key === 'value') {
						// ignore
					}
					else if (key === 'width') {
						cm.setSize(val, null);
					}
					else if (key === 'height') {
						cm.setSize(null, val);
					}
					else if (key === 'style') {
						cm.getWrapperElement().style = val;
					}
					else {
						cm.setOption(key, val);
					}
					break;

				case 'setEvent':
					var key = edit[type].name;
					var handler = salix.getNativeHandler(edit[type].handler);
					myHandlers[key] = handler;
					cm.on(key, handler);
					break

				case 'removeProp':
					var key = edit[type].name;
					if (key === 'width' || key === 'height') {
						// doesn't actually revert it.
						cm.setSize(null, null);
					}
					else if (key === 'style') {
						cm.getWrapperElement().style = '';
					}
					else {
						cm.setOption(key, CodeMirror.defaults[key]);
					}
					break;

				case 'removeEvent':
					var key = edit[type].name
					cm.off(key, myHandlers[key]);
					delete myHandlers[key];
					break;

				default:
					throw 'unsupported edit: ' + JSON.stringify(edit);

				}
			}
		}

		var dom = cm.getWrapperElement();
		dom.salix_native = {patch: patch};
	}

	function doCommand(cmd) {
		// TODO
	}

	salix.registerNative('codeMirror', codeMirror);
};<|endoftext|>