<commit_before>/**
 * Patterns parser - Argument parser
 *
 * Copyright 2012-2013 Florian Friesdorf
 * Copyright 2012-2013 Simplon B.V. - Wichert Akkerman
 */
define([
    "jquery",
    "pat-logger"
], function($, logger) {
    function ArgumentParser(name, opts) {
        opts = opts || {};
        this.inherit = typeof opts.inherit === "undefined" ?
            true : opts.inherit;
        this.order = [];
        this.parameters = {};
        this.attribute = "data-pat-" + name;
        this.enum_values = {};
        this.enum_conflicts = [];
        this.groups = {};
        this.possible_groups = {};
        this.log = logger.getLogger(name + ".parser");
    }

    ArgumentParser.prototype = {
        group_pattern: /([a-z][a-z0-9]*)-([A-Z][a-z0-0\-]*)/i,
        named_param_pattern: /^\s*([a-z][a-z0-9\-]*)\s*:(.*)/i,
        token_pattern: /((["']).*?(?!\\)\2)|\s*(\S+)\s*/g,

        _camelCase: function(str) {
            return str.replace(/\-([a-z])/g, function(_, p1){
                return p1.toUpperCase();
            });
        },

        add_argument: function(name, default_value, choices, multiple) {
            var spec, m;

            if (multiple && !Array.isArray(default_value))
                default_value=[default_value];
            spec={name: name,
                  value: default_value,
                  multiple: multiple,
                  dest: name,
                  group: null};

            if (choices && Array.isArray(choices) && choices.length) {
                spec.choices=choices;
                spec.type=this._typeof(choices[0]);
                for (var i=0; i<choices.length; i++)
                    if (this.enum_conflicts.indexOf(choices[i])!==-1)
                        continue;
                    else if (choices[i] in this.enum_values) {
                        this.enum_conflicts.push(choices[i]);
                        delete this.enum_values[choices[i]];
                    } else
                        this.enum_values[choices[i]]=name;
            } else if (typeof spec.value==="string" && spec.value.slice(0, 1)==="$")
                spec.type=this.parameters[spec.value.slice(1)].type;
            else
                // Note that this will get reset by _defaults if default_value is a function.
                spec.type=this._typeof(multiple ? spec.value[0] : spec.value);

            m=name.match(this.group_pattern);
            if (m) {
                var group=m[1], field=m[2];
                if (group in this.possible_groups) {
                    var first_spec = this.possible_groups[group],
                        first_name = first_spec.name.match(this.group_pattern)[2];
                    first_spec.group=group;
                    first_spec.dest=first_name;
                    this.groups[group]=new ArgumentParser();
                    this.groups[group].add_argument(
                            first_name,
                            spec.value, spec.cohices, spec.multiple);
                    delete this.possible_groups[group];
                }
                if (group in this.groups) {
                    this.groups[group].add_argument(field, default_value, choices, multiple);
                    spec.group=group;
                    spec.dest=field;
                } else {
                    this.possible_groups[group]=spec;
                    spec.dest=this._camelCase(spec.name);
                }
            }
            this.order.push(name);
            this.parameters[name]=spec;
        },

        _typeof: function(obj) {
            var type = typeof obj;
            if (obj===null)
                return "null";
            return type;
        },

        _coerce: function(name, value) {
            var spec=this.parameters[name];

            if (typeof value !== spec.type)
                try {
                    switch (spec.type) {
                        case "boolean":
                            if (typeof value === "string") {
                                value=value.toLowerCase();
                                var num = parseInt(value, 10);
                                if (!isNaN(num))
                                    value=!!num;
                                else
                                    value=(value==="true" || value==="y" || value==="yes" || value==="y");
                            } else if (typeof value === "number")
                                value=!!value;
                            else
                                throw ("Cannot convert value for " + name + " to boolean");
                            break;
                        case "number":
                            if (typeof value === "string") {
                                value=parseInt(value, 10);
                                if (isNaN(value))
                                    throw ("Cannot convert value for " + name + " to number");
                            } else if (typeof value === "boolean")
                                value=value + 0;
                            else
                                throw ("Cannot convert value for " + name + " to number");
                            break;
                        case "string":
                            value=value.toString();
                            break;
                        case "null":  // Missing default values
                        case "undefined":
                            break;
                        default:
                            throw ("Do not know how to convert value for " + name + " to " + spec.type);
                    }
                } catch (e) {
                    this.log.warn(e);
                    return null;
                }

            if (spec.choices && spec.choices.indexOf(value)===-1) {
                this.log.warn("Illegal value for " + name + ": " + value);
                return null;
            }

            return value;
        },

        _set: function(opts, name, value) {
            if (!(name in this.parameters)) {
                this.log.debug("Ignoring value for unknown argument " + name);
                return;
            }

            var spec=this.parameters[name];
            if (spec.multiple) {
                var parts=value.split(/,+/), i, v;
                value=[];
                for (i=0; i<parts.length; i++) {
                    v=this._coerce(name, parts[i].trim());
                    if (v!==null)
                        value.push(v);
                }
            } else {
                value=this._coerce(name, value);
                if (value===null)
                    return;
            }

            opts[name]=value;
        },

        _split: function(text) {
            var tokens = [];

            text.replace(this.token_pattern, function(match, quoted, _, simple) {
                if (quoted)
                    tokens.push(quoted);
                else if (simple)
                    tokens.push(simple);
            });
            return tokens;
        },

        _parseExtendedNotation: function(parameter) {
            var opts = {}, i,
                parts, matches;

            parts = parameter.replace(";;", "\xff")
                        .split(";")
                        .map(function(el) { return el.replace("\xff", ";"); });
            for (i=0; i<parts.length; i++) {
                if (!parts[i])
                    continue;

                matches = parts[i].match(this.named_param_pattern);
                if (!matches) {
                    this.log.warn("Invalid parameter: " + parts[i]);
                    break;
                }

                var name = matches[1],
                    value = matches[2].trim();

                if (name in this.parameters)
                    this._set(opts, name, value);
                else if (name in this.groups) {
                    var subopt = this.groups[name]._parseShorthandNotation(value);
                    for (var field in subopt)
                        this._set(opts, name+"-"+field, subopt[field]);
                } else {
                    this.log.warn("Unknown named parameter " + matches[1]);
                    continue;
                }
            }

            return opts;
        },

        _parseShorthandNotation: function(parameter) {
            var parts = this._split(parameter),
                opts = {},
                positional = true,
                i, part, flag, sense;

            i=0;
            while (parts.length) {
                part=parts.shift().trim();
                if (part.slice(0, 3)==="no-") {
                    sense=false;
                    flag=part.slice(3);
                } else {
                    sense=true;
                    flag=part;
                }
                if (flag in this.parameters && this.parameters[flag].type==="boolean") {
                    positional=false;
                    this._set(opts, flag, sense);
                } else if (flag in this.enum_values) {
                    positional=false;
                    this._set(opts, this.enum_values[flag], flag);
                } else if (positional)
                    this._set(opts, this.order[i], part);
                else {
                    parts.unshift(part);
                    break;
                }

                i++;
                if (i>=this.order.length)
                    break;
            }
            if (parts.length)
                this.log.warn("Ignore extra arguments: " + parts.join(" "));
            return opts;
        },

        _parse: function(parameter) {
            var opts, extended, sep;

            if (!parameter)
                return {};

            if (parameter.match(this.named_param_pattern))
                return this._parseExtendedNotation(parameter);

            sep=parameter.indexOf(";");
            if (sep===-1)
                return this._parseShorthandNotation(parameter);

            opts=this._parseShorthandNotation(parameter.slice(0, sep));
            extended=this._parseExtendedNotation(parameter.slice(sep+1));
            for (var name in extended)
                opts[name]=extended[name];
            return opts;
        },

        _defaults: function($el) {
            var result = {};
            for (var name in this.parameters)
                if (typeof this.parameters[name].value==="function")
                    try {
                        result[name]=this.parameters[name].value($el, name);
                        this.parameters[name].type=typeof result[name];
                    } catch(e) {
                        this.log.error("Default function for " + name + " failed.");
                    }
                else
                    result[name]=this.parameters[name].value;
            return result;
        },

        _cleanupOptions: function(options) {
            var keys = Object.keys(options),
                i, spec, name, target;

            // Resolve references
            for (i=0; i<keys.length; i++) {
                name=keys[i];
                spec=this.parameters[name];
                if (spec===undefined)
                    continue;

                if (options[name]===spec.value &&
                        typeof spec.value==="string" && spec.value.slice(0, 1)==="$")
                    options[name]=options[spec.value.slice(1)];
            }

            // Move options into groups and do renames
            keys=Object.keys(options);
            for (i=0; i<keys.length; i++) {
                name=keys[i];
                spec=this.parameters[name];
                if (spec===undefined)
                    continue;

                if (spec.group)  {
                    if (typeof options[spec.group]!=="object")
                        options[spec.group]={};
                    target=options[spec.group];
                } else
                    target=options;

                if (spec.dest!==name) {
                    target[spec.dest]=options[name];
                    delete options[name];
                }
            }
        },

        parse: function($el, options, multiple) {
            if (typeof options==="boolean" && multiple===undefined) {
                multiple=options;
                options={};
            }

            var stack = [[this._defaults($el)]];

            var $possible_config_providers = this.inherit ?
                    $el.parents().andSelf() : $el,
                final_length = 1,
                i, data, frame;
            for (i=0; i<$possible_config_providers.length; i++) {
                data = $possible_config_providers.eq(i).attr(this.attribute);
                if (data) {
                    var _parse = this._parse.bind(this); // Needed to fix binding in map call
                    if (data.match(/&&/))
                        frame=data.split(/\s*&&\s*/).map(_parse);
                    else
                        frame=[_parse(data)];
                    final_length = Math.max(frame.length, final_length);
                    stack.push(frame);
                }
            }
            if (typeof options==="object") {
                if (Array.isArray(options)) {
                    stack.push(options);
                    final_length=Math.max(options.length, final_length);
                } else
                    stack.push([options]);
            }

            if (!multiple)
                final_length=1;

            var results=[], frame_length, x, xf;
            for (i=0; i<final_length; i++)
                results.push({});

            for (i=0; i<stack.length; i++) {
                frame=stack[i];
                frame_length=frame.length-1;

                for (x=0; x<final_length; x++) {
                    xf=(x>frame_length) ? frame_length : x;
                    results[x]=$.extend(results[x], frame[xf]);
                }
            }

            for (i=0; i<results.length; i++)
                this._cleanupOptions(results[i]);

            return multiple ? results : results[0];
        }
    };

    return ArgumentParser;
});
// jshint indent: 4, browser: true, jquery: true, quotmark: double
// vim: sw=4 expandtab
<commit_msg>Parser: make inherit a parse-time flag<commit_after>/**
 * Patterns parser - Argument parser
 *
 * Copyright 2012-2013 Florian Friesdorf
 * Copyright 2012-2013 Simplon B.V. - Wichert Akkerman
 */
define([
    "jquery",
    "pat-logger"
], function($, logger) {
    function ArgumentParser(name, opts) {
        opts = opts || {};
        this.order = [];
        this.parameters = {};
        this.attribute = "data-pat-" + name;
        this.enum_values = {};
        this.enum_conflicts = [];
        this.groups = {};
        this.possible_groups = {};
        this.log = logger.getLogger(name + ".parser");
    }

    ArgumentParser.prototype = {
        group_pattern: /([a-z][a-z0-9]*)-([A-Z][a-z0-0\-]*)/i,
        named_param_pattern: /^\s*([a-z][a-z0-9\-]*)\s*:(.*)/i,
        token_pattern: /((["']).*?(?!\\)\2)|\s*(\S+)\s*/g,

        _camelCase: function(str) {
            return str.replace(/\-([a-z])/g, function(_, p1){
                return p1.toUpperCase();
            });
        },

        add_argument: function(name, default_value, choices, multiple) {
            var spec, m;

            if (multiple && !Array.isArray(default_value))
                default_value=[default_value];
            spec={name: name,
                  value: default_value,
                  multiple: multiple,
                  dest: name,
                  group: null};

            if (choices && Array.isArray(choices) && choices.length) {
                spec.choices=choices;
                spec.type=this._typeof(choices[0]);
                for (var i=0; i<choices.length; i++)
                    if (this.enum_conflicts.indexOf(choices[i])!==-1)
                        continue;
                    else if (choices[i] in this.enum_values) {
                        this.enum_conflicts.push(choices[i]);
                        delete this.enum_values[choices[i]];
                    } else
                        this.enum_values[choices[i]]=name;
            } else if (typeof spec.value==="string" && spec.value.slice(0, 1)==="$")
                spec.type=this.parameters[spec.value.slice(1)].type;
            else
                // Note that this will get reset by _defaults if default_value is a function.
                spec.type=this._typeof(multiple ? spec.value[0] : spec.value);

            m=name.match(this.group_pattern);
            if (m) {
                var group=m[1], field=m[2];
                if (group in this.possible_groups) {
                    var first_spec = this.possible_groups[group],
                        first_name = first_spec.name.match(this.group_pattern)[2];
                    first_spec.group=group;
                    first_spec.dest=first_name;
                    this.groups[group]=new ArgumentParser();
                    this.groups[group].add_argument(
                            first_name,
                            spec.value, spec.cohices, spec.multiple);
                    delete this.possible_groups[group];
                }
                if (group in this.groups) {
                    this.groups[group].add_argument(field, default_value, choices, multiple);
                    spec.group=group;
                    spec.dest=field;
                } else {
                    this.possible_groups[group]=spec;
                    spec.dest=this._camelCase(spec.name);
                }
            }
            this.order.push(name);
            this.parameters[name]=spec;
        },

        _typeof: function(obj) {
            var type = typeof obj;
            if (obj===null)
                return "null";
            return type;
        },

        _coerce: function(name, value) {
            var spec=this.parameters[name];

            if (typeof value !== spec.type)
                try {
                    switch (spec.type) {
                        case "boolean":
                            if (typeof value === "string") {
                                value=value.toLowerCase();
                                var num = parseInt(value, 10);
                                if (!isNaN(num))
                                    value=!!num;
                                else
                                    value=(value==="true" || value==="y" || value==="yes" || value==="y");
                            } else if (typeof value === "number")
                                value=!!value;
                            else
                                throw ("Cannot convert value for " + name + " to boolean");
                            break;
                        case "number":
                            if (typeof value === "string") {
                                value=parseInt(value, 10);
                                if (isNaN(value))
                                    throw ("Cannot convert value for " + name + " to number");
                            } else if (typeof value === "boolean")
                                value=value + 0;
                            else
                                throw ("Cannot convert value for " + name + " to number");
                            break;
                        case "string":
                            value=value.toString();
                            break;
                        case "null":  // Missing default values
                        case "undefined":
                            break;
                        default:
                            throw ("Do not know how to convert value for " + name + " to " + spec.type);
                    }
                } catch (e) {
                    this.log.warn(e);
                    return null;
                }

            if (spec.choices && spec.choices.indexOf(value)===-1) {
                this.log.warn("Illegal value for " + name + ": " + value);
                return null;
            }

            return value;
        },

        _set: function(opts, name, value) {
            if (!(name in this.parameters)) {
                this.log.debug("Ignoring value for unknown argument " + name);
                return;
            }

            var spec=this.parameters[name];
            if (spec.multiple) {
                var parts=value.split(/,+/), i, v;
                value=[];
                for (i=0; i<parts.length; i++) {
                    v=this._coerce(name, parts[i].trim());
                    if (v!==null)
                        value.push(v);
                }
            } else {
                value=this._coerce(name, value);
                if (value===null)
                    return;
            }

            opts[name]=value;
        },

        _split: function(text) {
            var tokens = [];

            text.replace(this.token_pattern, function(match, quoted, _, simple) {
                if (quoted)
                    tokens.push(quoted);
                else if (simple)
                    tokens.push(simple);
            });
            return tokens;
        },

        _parseExtendedNotation: function(parameter) {
            var opts = {}, i,
                parts, matches;

            parts = parameter.replace(";;", "\xff")
                        .split(";")
                        .map(function(el) { return el.replace("\xff", ";"); });
            for (i=0; i<parts.length; i++) {
                if (!parts[i])
                    continue;

                matches = parts[i].match(this.named_param_pattern);
                if (!matches) {
                    this.log.warn("Invalid parameter: " + parts[i]);
                    break;
                }

                var name = matches[1],
                    value = matches[2].trim();

                if (name in this.parameters)
                    this._set(opts, name, value);
                else if (name in this.groups) {
                    var subopt = this.groups[name]._parseShorthandNotation(value);
                    for (var field in subopt)
                        this._set(opts, name+"-"+field, subopt[field]);
                } else {
                    this.log.warn("Unknown named parameter " + matches[1]);
                    continue;
                }
            }

            return opts;
        },

        _parseShorthandNotation: function(parameter) {
            var parts = this._split(parameter),
                opts = {},
                positional = true,
                i, part, flag, sense;

            i=0;
            while (parts.length) {
                part=parts.shift().trim();
                if (part.slice(0, 3)==="no-") {
                    sense=false;
                    flag=part.slice(3);
                } else {
                    sense=true;
                    flag=part;
                }
                if (flag in this.parameters && this.parameters[flag].type==="boolean") {
                    positional=false;
                    this._set(opts, flag, sense);
                } else if (flag in this.enum_values) {
                    positional=false;
                    this._set(opts, this.enum_values[flag], flag);
                } else if (positional)
                    this._set(opts, this.order[i], part);
                else {
                    parts.unshift(part);
                    break;
                }

                i++;
                if (i>=this.order.length)
                    break;
            }
            if (parts.length)
                this.log.warn("Ignore extra arguments: " + parts.join(" "));
            return opts;
        },

        _parse: function(parameter) {
            var opts, extended, sep;

            if (!parameter)
                return {};

            if (parameter.match(this.named_param_pattern))
                return this._parseExtendedNotation(parameter);

            sep=parameter.indexOf(";");
            if (sep===-1)
                return this._parseShorthandNotation(parameter);

            opts=this._parseShorthandNotation(parameter.slice(0, sep));
            extended=this._parseExtendedNotation(parameter.slice(sep+1));
            for (var name in extended)
                opts[name]=extended[name];
            return opts;
        },

        _defaults: function($el) {
            var result = {};
            for (var name in this.parameters)
                if (typeof this.parameters[name].value==="function")
                    try {
                        result[name]=this.parameters[name].value($el, name);
                        this.parameters[name].type=typeof result[name];
                    } catch(e) {
                        this.log.error("Default function for " + name + " failed.");
                    }
                else
                    result[name]=this.parameters[name].value;
            return result;
        },

        _cleanupOptions: function(options) {
            var keys = Object.keys(options),
                i, spec, name, target;

            // Resolve references
            for (i=0; i<keys.length; i++) {
                name=keys[i];
                spec=this.parameters[name];
                if (spec===undefined)
                    continue;

                if (options[name]===spec.value &&
                        typeof spec.value==="string" && spec.value.slice(0, 1)==="$")
                    options[name]=options[spec.value.slice(1)];
            }

            // Move options into groups and do renames
            keys=Object.keys(options);
            for (i=0; i<keys.length; i++) {
                name=keys[i];
                spec=this.parameters[name];
                if (spec===undefined)
                    continue;

                if (spec.group)  {
                    if (typeof options[spec.group]!=="object")
                        options[spec.group]={};
                    target=options[spec.group];
                } else
                    target=options;

                if (spec.dest!==name) {
                    target[spec.dest]=options[name];
                    delete options[name];
                }
            }
        },

        parse: function($el, options, multiple, inherit) {
            if (typeof options==="boolean" && multiple===undefined) {
                multiple=options;
                options={};
            }

            var stack = [[this._defaults($el)]];

            var $possible_config_providers = (inherit!==false) ? $el.parents().andSelf() : $el,
                final_length = 1,
                i, data, frame;
            for (i=0; i<$possible_config_providers.length; i++) {
                data = $possible_config_providers.eq(i).attr(this.attribute);
                if (data) {
                    var _parse = this._parse.bind(this); // Needed to fix binding in map call
                    if (data.match(/&&/))
                        frame=data.split(/\s*&&\s*/).map(_parse);
                    else
                        frame=[_parse(data)];
                    final_length = Math.max(frame.length, final_length);
                    stack.push(frame);
                }
            }
            if (typeof options==="object") {
                if (Array.isArray(options)) {
                    stack.push(options);
                    final_length=Math.max(options.length, final_length);
                } else
                    stack.push([options]);
            }

            if (!multiple)
                final_length=1;

            var results=[], frame_length, x, xf;
            for (i=0; i<final_length; i++)
                results.push({});

            for (i=0; i<stack.length; i++) {
                frame=stack[i];
                frame_length=frame.length-1;

                for (x=0; x<final_length; x++) {
                    xf=(x>frame_length) ? frame_length : x;
                    results[x]=$.extend(results[x], frame[xf]);
                }
            }

            for (i=0; i<results.length; i++)
                this._cleanupOptions(results[i]);

            return multiple ? results : results[0];
        }
    };

    return ArgumentParser;
});
// jshint indent: 4, browser: true, jquery: true, quotmark: double
// vim: sw=4 expandtab
<|endoftext|>


<commit_before>/**
 * Patterns parser - Argument parser
 *
 * Copyright 2012-2013 Florian Friesdorf
 * Copyright 2012-2013 Simplon B.V. - Wichert Akkerman
 */
define([
    'jquery',
    './logger'
], function($, logger) {
    function ArgumentParser(name) {
        this.order = [];
        this.parameters = {};
        this.attribute = "data-pat-" + name;
        this.enum_values = {};
        this.enum_conflicts = [];
        this.groups = {};
        this.possible_groups = {};
        this.log = logger.getLogger(name + ".parser");
    }

    ArgumentParser.prototype = {
        group_pattern: /([a-z][a-z0-9]*)-([A-Z][a-z0-0\-]*)/i,
        named_param_pattern: /^\s*([a-z][a-z0-9\-]*)\s*:(.*)/i,

        _camelCase: function(str) {
            return str.replace(/\-([a-z])/g, function(_, p1){
                return p1.toUpperCase();
            });
        },

        add_argument: function(name, default_value, choices, multiple) {
            var spec, m;

            if (multiple && !Array.isArray(default_value))
                default_value=[default_value];
            spec={name: name,
                  value: default_value,
                  multiple: multiple,
                  dest: name,
                  group: null};

            if (choices && Array.isArray(choices) && choices.length) {
                spec.choices=choices;
                spec.type=this._typeof(choices[0]);
                for (var i=0; i<choices.length; i++)
                    if (this.enum_conflicts.indexOf(choices[i])!==-1)
                        continue;
                    else if (choices[i] in this.enum_values) {
                        this.enum_conflicts.push(choices[i]);
                        delete this.enum_values[choices[i]];
                    } else
                        this.enum_values[choices[i]]=name;
            } else if (typeof spec.value==="string" && spec.value.slice(0, 1)==="$")
                spec.type=this.parameters[spec.value.slice(1)].type;
            else
                // Note that this will get reset by _defaults if default_value is a function.
                spec.type=this._typeof(multiple ? spec.value[0] : spec.value);

            m=name.match(this.group_pattern);
            if (m) {
                var group=m[1], field=m[2];
                if (group in this.possible_groups) {
                    var first_spec = this.possible_groups[group],
                        first_name = first_spec.name.match(this.group_pattern)[2];
                    first_spec.group=group;
                    first_spec.dest=first_name;
                    this.groups[group]=new ArgumentParser();
                    this.groups[group].add_argument(
                            first_name,
                            spec.value, spec.cohices, spec.multiple);
                    delete this.possible_groups[group];
                }
                if (group in this.groups) {
                    this.groups[group].add_argument(field, default_value, choices, multiple);
                    spec.group=group;
                    spec.dest=field;
                } else {
                    this.possible_groups[group]=spec;
                    spec.dest=this._camelCase(spec.name);
                }
            }
            this.order.push(name);
            this.parameters[name]=spec;
        },

        _typeof: function(obj) {
            var type = typeof obj;
            if (obj===null)
                return "null";
            return type;
        },

        _coerce: function(name, value) {
            var spec=this.parameters[name];

            if (typeof value !== spec.type)
                try {
                    switch (spec.type) {
                        case "boolean":
                            if (typeof value === "string") {
                                value=value.toLowerCase();
                                var num = parseInt(value, 10);
                                if (!isNaN(num))
                                    value=!!num;
                                else
                                    value=(value==="true" || value==="y" || value==="yes" || value==="y");
                            } else if (typeof value === "number")
                                value=!!value;
                            else
                                throw ("Cannot convert value for " + name + " to boolean");
                            break;
                        case "number":
                            if (typeof value === "string") {
                                value=parseInt(value, 10);
                                if (isNaN(value))
                                    throw ("Cannot convert value for " + name + " to number");
                            } else if (typeof value === "boolean")
                                value=value + 0;
                            else
                                throw ("Cannot convert value for " + name + " to number");
                            break;
                        case "string":
                            value=value.toString();
                            break;
                        case "null":  // Missing default values
                        case "undefined":
                            break;
                        default:
                            throw ("Do not know how to convert value for " + name + " to " + spec.type);
                    }
                } catch (e) {
                    this.log.warn(e);
                    return null;
                }

            if (spec.choices && spec.choices.indexOf(value)===-1) {
                this.log.warn("Illegal value for " + name + ": " + value);
                return null;
            }

            return value;
        },

        _set: function(opts, name, value) {
            if (!(name in this.parameters)) {
                this.log.debug("Ignoring value for unknown argument " + name);
                return;
            }

            var spec=this.parameters[name];
            if (spec.multiple) {
                var parts=value.split(/,+/), i, v;
                value=[];
                for (i=0; i<parts.length; i++) {
                    v=this._coerce(name, parts[i].trim());
                    if (v!==null)
                        value.push(v);
                }
            } else {
                value=this._coerce(name, value);
                if (value===null)
                    return;
            }

            opts[name]=value;
        },

        _parseExtendedNotation: function(parameter) {
            var opts = {}, i,
                parts, matches;

            parts = parameter.replace(";;", "\xff")
                        .split(";")
                        .map(function(el) { return el.replace("\xff", ";") });
            for (i=0; i<parts.length; i++) {
                if (!parts[i])
                    continue;

                matches = parts[i].match(this.named_param_pattern);
                if (!matches) {
                    this.log.warn("Invalid parameter: " + parts[i]);
                    break;
                }

                var name = matches[1],
                    value = matches[2].trim();

                if (name in this.parameters)
                    this._set(opts, name, value);
                else if (name in this.groups) {
                    var subopt = this.groups[name]._parseShorthandNotation(value);
                    for (var field in subopt)
                        this._set(opts, name+"-"+field, subopt[field]);
                } else {
                    this.log.warn("Unknown named parameter " + matches[1]);
                    continue;
                }
            }

            return opts;
        },

        _parseShorthandNotation: function(parameter) {
            var parts = parameter.split(/\s+/),
                opts = {},
                positional = true,
                i, part, flag, sense, matches;

            i=0;
            while (parts.length) {
                part=parts.shift().trim();
                if (part.slice(0, 3)==="no-") {
                    sense=false;
                    flag=part.slice(3);
                } else {
                    sense=true;
                    flag=part;
                }
                if (flag in this.parameters && this.parameters[flag].type==="boolean") {
                    positional=false;
                    this._set(opts, flag, sense);
                } else if (flag in this.enum_values) {
                    positional=false;
                    this._set(opts, this.enum_values[flag], flag);
                } else if (positional)
                    this._set(opts, this.order[i], part);
                else {
                    parts.unshift(part);
                    break;
                }

                i++;
                if (i>=this.order.length)
                    break;
            }
            if (parts.length)
                this.log.warn("Ignore extra arguments: " + parts.join(" "));
            return opts;
        },

        _parse: function(parameter) {
            var opts, extended, sep;

            if (!parameter)
                return {};

            if (parameter.match(this.named_param_pattern))
                return this._parseExtendedNotation(parameter);

            sep=parameter.indexOf(";");
            if (sep===-1)
                return this._parseShorthandNotation(parameter);

            opts=this._parseShorthandNotation(parameter.slice(0, sep));
            extended=this._parseExtendedNotation(parameter.slice(sep+1));
            for (var name in extended)
                opts[name]=extended[name];
            return opts;
        },

        _defaults: function($el) {
            var result = {};
            for (var name in this.parameters)
                if (typeof this.parameters[name].value==="function")
                    try {
                        result[name]=this.parameters[name].value($el, name);
                        this.parameters[name].type=typeof result[name];
                    } catch(e) {
                        this.log.error("Default function for " + name + " failed.");
                    }
                else
                    result[name]=this.parameters[name].value;
            return result;
        },

        _cleanupOptions: function(options) {
            var keys = Object.keys(options),
                i, spec, name, group;

            // Resolve references
            for (i=0; i<keys.length; i++) {
                name=keys[i];
                spec=this.parameters[name];
                if (spec===undefined)
                    continue;

                if (options[name]===spec.value &&
                        typeof spec.value==="string" && spec.value.slice(0, 1)==="$")
                    options[name]=options[spec.value.slice(1)];
            }

            // Move options into groups and do renames
            keys=Object.keys(options);
            for (i=0; i<keys.length; i++) {
                name=keys[i];
                spec=this.parameters[name];
                if (spec===undefined)
                    continue;

                if (spec.group)  {
                    if (typeof options[spec.group]!=="object")
                        options[spec.group]={};
                    target=options[spec.group];
                } else
                    target=options;

                if (spec.dest!==name) {
                    target[spec.dest]=options[name];
                    delete options[name];
                }
            }
        },

        parse: function($el, options, multiple) {
            if (typeof options==="boolean" && multiple===undefined) {
                multiple=options;
                options={};
            }

            var stack = [[this._defaults($el)]];

            var $parents = $el.parents().andSelf(),
                final_length = 1,
                i, data, frame;
            for (i=0; i<$parents.length; i++) {
                data = $parents.eq(i).attr(this.attribute);
                if (data) {
                    var _parse = this._parse.bind(this); // Needed to fix binding in map call
                    if (data.match(/&&/))
                        frame=data.split(/\s*&&\s*/).map(_parse);
                    else
                        frame=[_parse(data)];
                    final_length = Math.max(frame.length, final_length);
                    stack.push(frame);
                }
            }
            if (typeof options==="object") {
                if (Array.isArray(options)) {
                    stack.push(options);
                    final_length=Math.max(options.length, final_length);
                } else
                    stack.push([options]);
            }

            if (!multiple)
                final_length=1;

            var results=[], frame_length, x, xf;
            for (i=0; i<final_length; i++)
                results.push({});

            for (i=0; i<stack.length; i++) {
                frame=stack[i];
                frame_length=frame.length-1;

                for (x=0; x<final_length; x++) {
                    xf=(x>frame_length) ? frame_length : x;
                    results[x]=$.extend(results[x], frame[xf]);
                }
            }

            for (i=0; i<results.length; i++)
                this._cleanupOptions(results[i]);

            return multiple ? results : results[0];
        }
    };

    return ArgumentParser;
});
// jshint indent: 4, browser: true, jquery: true, quotmark: double
// vim: sw=4 expandtab
<commit_msg>Jshint fix.<commit_after>/**
 * Patterns parser - Argument parser
 *
 * Copyright 2012-2013 Florian Friesdorf
 * Copyright 2012-2013 Simplon B.V. - Wichert Akkerman
 */
define([
    'jquery',
    './logger'
], function($, logger) {
    function ArgumentParser(name) {
        this.order = [];
        this.parameters = {};
        this.attribute = "data-pat-" + name;
        this.enum_values = {};
        this.enum_conflicts = [];
        this.groups = {};
        this.possible_groups = {};
        this.log = logger.getLogger(name + ".parser");
    }

    ArgumentParser.prototype = {
        group_pattern: /([a-z][a-z0-9]*)-([A-Z][a-z0-0\-]*)/i,
        named_param_pattern: /^\s*([a-z][a-z0-9\-]*)\s*:(.*)/i,

        _camelCase: function(str) {
            return str.replace(/\-([a-z])/g, function(_, p1){
                return p1.toUpperCase();
            });
        },

        add_argument: function(name, default_value, choices, multiple) {
            var spec, m;

            if (multiple && !Array.isArray(default_value))
                default_value=[default_value];
            spec={name: name,
                  value: default_value,
                  multiple: multiple,
                  dest: name,
                  group: null};

            if (choices && Array.isArray(choices) && choices.length) {
                spec.choices=choices;
                spec.type=this._typeof(choices[0]);
                for (var i=0; i<choices.length; i++)
                    if (this.enum_conflicts.indexOf(choices[i])!==-1)
                        continue;
                    else if (choices[i] in this.enum_values) {
                        this.enum_conflicts.push(choices[i]);
                        delete this.enum_values[choices[i]];
                    } else
                        this.enum_values[choices[i]]=name;
            } else if (typeof spec.value==="string" && spec.value.slice(0, 1)==="$")
                spec.type=this.parameters[spec.value.slice(1)].type;
            else
                // Note that this will get reset by _defaults if default_value is a function.
                spec.type=this._typeof(multiple ? spec.value[0] : spec.value);

            m=name.match(this.group_pattern);
            if (m) {
                var group=m[1], field=m[2];
                if (group in this.possible_groups) {
                    var first_spec = this.possible_groups[group],
                        first_name = first_spec.name.match(this.group_pattern)[2];
                    first_spec.group=group;
                    first_spec.dest=first_name;
                    this.groups[group]=new ArgumentParser();
                    this.groups[group].add_argument(
                            first_name,
                            spec.value, spec.cohices, spec.multiple);
                    delete this.possible_groups[group];
                }
                if (group in this.groups) {
                    this.groups[group].add_argument(field, default_value, choices, multiple);
                    spec.group=group;
                    spec.dest=field;
                } else {
                    this.possible_groups[group]=spec;
                    spec.dest=this._camelCase(spec.name);
                }
            }
            this.order.push(name);
            this.parameters[name]=spec;
        },

        _typeof: function(obj) {
            var type = typeof obj;
            if (obj===null)
                return "null";
            return type;
        },

        _coerce: function(name, value) {
            var spec=this.parameters[name];

            if (typeof value !== spec.type)
                try {
                    switch (spec.type) {
                        case "boolean":
                            if (typeof value === "string") {
                                value=value.toLowerCase();
                                var num = parseInt(value, 10);
                                if (!isNaN(num))
                                    value=!!num;
                                else
                                    value=(value==="true" || value==="y" || value==="yes" || value==="y");
                            } else if (typeof value === "number")
                                value=!!value;
                            else
                                throw ("Cannot convert value for " + name + " to boolean");
                            break;
                        case "number":
                            if (typeof value === "string") {
                                value=parseInt(value, 10);
                                if (isNaN(value))
                                    throw ("Cannot convert value for " + name + " to number");
                            } else if (typeof value === "boolean")
                                value=value + 0;
                            else
                                throw ("Cannot convert value for " + name + " to number");
                            break;
                        case "string":
                            value=value.toString();
                            break;
                        case "null":  // Missing default values
                        case "undefined":
                            break;
                        default:
                            throw ("Do not know how to convert value for " + name + " to " + spec.type);
                    }
                } catch (e) {
                    this.log.warn(e);
                    return null;
                }

            if (spec.choices && spec.choices.indexOf(value)===-1) {
                this.log.warn("Illegal value for " + name + ": " + value);
                return null;
            }

            return value;
        },

        _set: function(opts, name, value) {
            if (!(name in this.parameters)) {
                this.log.debug("Ignoring value for unknown argument " + name);
                return;
            }

            var spec=this.parameters[name];
            if (spec.multiple) {
                var parts=value.split(/,+/), i, v;
                value=[];
                for (i=0; i<parts.length; i++) {
                    v=this._coerce(name, parts[i].trim());
                    if (v!==null)
                        value.push(v);
                }
            } else {
                value=this._coerce(name, value);
                if (value===null)
                    return;
            }

            opts[name]=value;
        },

        _parseExtendedNotation: function(parameter) {
            var opts = {}, i,
                parts, matches;

            parts = parameter.replace(";;", "\xff")
                        .split(";")
                        .map(function(el) { return el.replace("\xff", ";"); });
            for (i=0; i<parts.length; i++) {
                if (!parts[i])
                    continue;

                matches = parts[i].match(this.named_param_pattern);
                if (!matches) {
                    this.log.warn("Invalid parameter: " + parts[i]);
                    break;
                }

                var name = matches[1],
                    value = matches[2].trim();

                if (name in this.parameters)
                    this._set(opts, name, value);
                else if (name in this.groups) {
                    var subopt = this.groups[name]._parseShorthandNotation(value);
                    for (var field in subopt)
                        this._set(opts, name+"-"+field, subopt[field]);
                } else {
                    this.log.warn("Unknown named parameter " + matches[1]);
                    continue;
                }
            }

            return opts;
        },

        _parseShorthandNotation: function(parameter) {
            var parts = parameter.split(/\s+/),
                opts = {},
                positional = true,
                i, part, flag, sense, matches;

            i=0;
            while (parts.length) {
                part=parts.shift().trim();
                if (part.slice(0, 3)==="no-") {
                    sense=false;
                    flag=part.slice(3);
                } else {
                    sense=true;
                    flag=part;
                }
                if (flag in this.parameters && this.parameters[flag].type==="boolean") {
                    positional=false;
                    this._set(opts, flag, sense);
                } else if (flag in this.enum_values) {
                    positional=false;
                    this._set(opts, this.enum_values[flag], flag);
                } else if (positional)
                    this._set(opts, this.order[i], part);
                else {
                    parts.unshift(part);
                    break;
                }

                i++;
                if (i>=this.order.length)
                    break;
            }
            if (parts.length)
                this.log.warn("Ignore extra arguments: " + parts.join(" "));
            return opts;
        },

        _parse: function(parameter) {
            var opts, extended, sep;

            if (!parameter)
                return {};

            if (parameter.match(this.named_param_pattern))
                return this._parseExtendedNotation(parameter);

            sep=parameter.indexOf(";");
            if (sep===-1)
                return this._parseShorthandNotation(parameter);

            opts=this._parseShorthandNotation(parameter.slice(0, sep));
            extended=this._parseExtendedNotation(parameter.slice(sep+1));
            for (var name in extended)
                opts[name]=extended[name];
            return opts;
        },

        _defaults: function($el) {
            var result = {};
            for (var name in this.parameters)
                if (typeof this.parameters[name].value==="function")
                    try {
                        result[name]=this.parameters[name].value($el, name);
                        this.parameters[name].type=typeof result[name];
                    } catch(e) {
                        this.log.error("Default function for " + name + " failed.");
                    }
                else
                    result[name]=this.parameters[name].value;
            return result;
        },

        _cleanupOptions: function(options) {
            var keys = Object.keys(options),
                i, spec, name, group;

            // Resolve references
            for (i=0; i<keys.length; i++) {
                name=keys[i];
                spec=this.parameters[name];
                if (spec===undefined)
                    continue;

                if (options[name]===spec.value &&
                        typeof spec.value==="string" && spec.value.slice(0, 1)==="$")
                    options[name]=options[spec.value.slice(1)];
            }

            // Move options into groups and do renames
            keys=Object.keys(options);
            for (i=0; i<keys.length; i++) {
                name=keys[i];
                spec=this.parameters[name];
                if (spec===undefined)
                    continue;

                if (spec.group)  {
                    if (typeof options[spec.group]!=="object")
                        options[spec.group]={};
                    target=options[spec.group];
                } else
                    target=options;

                if (spec.dest!==name) {
                    target[spec.dest]=options[name];
                    delete options[name];
                }
            }
        },

        parse: function($el, options, multiple) {
            if (typeof options==="boolean" && multiple===undefined) {
                multiple=options;
                options={};
            }

            var stack = [[this._defaults($el)]];

            var $parents = $el.parents().andSelf(),
                final_length = 1,
                i, data, frame;
            for (i=0; i<$parents.length; i++) {
                data = $parents.eq(i).attr(this.attribute);
                if (data) {
                    var _parse = this._parse.bind(this); // Needed to fix binding in map call
                    if (data.match(/&&/))
                        frame=data.split(/\s*&&\s*/).map(_parse);
                    else
                        frame=[_parse(data)];
                    final_length = Math.max(frame.length, final_length);
                    stack.push(frame);
                }
            }
            if (typeof options==="object") {
                if (Array.isArray(options)) {
                    stack.push(options);
                    final_length=Math.max(options.length, final_length);
                } else
                    stack.push([options]);
            }

            if (!multiple)
                final_length=1;

            var results=[], frame_length, x, xf;
            for (i=0; i<final_length; i++)
                results.push({});

            for (i=0; i<stack.length; i++) {
                frame=stack[i];
                frame_length=frame.length-1;

                for (x=0; x<final_length; x++) {
                    xf=(x>frame_length) ? frame_length : x;
                    results[x]=$.extend(results[x], frame[xf]);
                }
            }

            for (i=0; i<results.length; i++)
                this._cleanupOptions(results[i]);

            return multiple ? results : results[0];
        }
    };

    return ArgumentParser;
});
// jshint indent: 4, browser: true, jquery: true, quotmark: double
// vim: sw=4 expandtab
<|endoftext|>


<commit_before>/**
 * @license
 * Patterns @VERSION@ parser - argument parser
 *
 * Copyright 2012 Simplon B.V.
 * Copyright 2012 Florian Friesdorf
 */
define([], function() {

    // older browsers - factor out if more need it
    // source: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/Trim
    if (!String.prototype.trim) {
        String.prototype.trim = function () {
            return this.replace(/^\s+|\s+$/g,'');
        };
    }

    // XXX: move somewhere
    // source: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/map
    // Production steps of ECMA-262, Edition 5, 15.4.4.19
    // Reference: http://es5.github.com/#x15.4.4.19
    if (!Array.prototype.map) {
        Array.prototype.map = function(callback, thisArg) {

            var T, A, k;

            if (this == null) {
                throw new TypeError(" this is null or not defined");
            }

            // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
            var O = Object(this);

            // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
            // 3. Let len be ToUint32(lenValue).
            var len = O.length >>> 0;

            // 4. If IsCallable(callback) is false, throw a TypeError exception.
            // See: http://es5.github.com/#x9.11
            if ({}.toString.call(callback) != "[object Function]") {
                throw new TypeError(callback + " is not a function");
            }

            // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
            if (thisArg) {
                T = thisArg;
            }

            // 6. Let A be a new array created as if by the expression new Array(len) where Array is
            // the standard built-in constructor with that name and len is the value of len.
            A = new Array(len);

            // 7. Let k be 0
            k = 0;

            // 8. Repeat, while k < len
            while(k < len) {

                var kValue, mappedValue;

                // a. Let Pk be ToString(k).
                //   This is implicit for LHS operands of the in operator
                // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
                //   This step can be combined with c
                // c. If kPresent is true, then
                if (k in O) {

                    // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
                    kValue = O[ k ];

                    // ii. Let mappedValue be the result of calling the Call internal method of callback
                    // with T as the this value and argument list containing kValue, k, and O.
                    mappedValue = callback.call(T, kValue, k, O);

                    // iii. Call the DefineOwnProperty internal method of A with arguments
                    // Pk, Property Descriptor {Value: mappedValue, Writable: true, Enumerable: true, Configurable: true},
                    // and false.

                    // In browsers that support Object.defineProperty, use the following:
                    // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });

                    // For best browser support, use the following:
                    A[ k ] = mappedValue;
                }
                // d. Increase k by 1.
                k++;
            }

            // 9. return A
            return A;
        };
    }

    // XXX: move to dedicated module
    var warn = function(msg) {
        try {
            console.warn(msg);
        } catch(e) {
        }
    };

    function ArgumentParser(spec) {
        this.params = [];
        this.named_params = {};
        if (spec) {
            var parts = spec.split(';'),
                parser = this;
            for (var i=0, part; i<parts.length; i++) {
                part = parts[i].trim();
                var match = part.match(parser.named_param_pattern);
                if (match) {
                    parser.add_argument(match[1], match[2].trim());
                } else {
                    parser.add_argument(part);
                }
            }
        }
    };
    ArgumentParser.prototype = {
        named_param_pattern: /^\s*([a-zA-z]+)\s*:(.*)/,

        add_argument: function(name, default_value) {
            var parameter = {"name" : name,
                             "default": default_value};
            this.params.push(parameter);
            this.named_params[name] = parameter;
        },

        parse: function(parameter) {
            if (parameter.match(/&&/)) {
                return parameter.split(/\s*&&\s*/).map(this.parse, this);
            }

            var parts = parameter.split(";"),
                opts = {},
                part, matches, i, name;

            // Popuplate options with default values
            for (i=0; i<this.params.length; i++) {
                name = this.params[i].name;
                opts[name] = this.params[i].default;
            }

            // Grab all positional parameters
            i=-1;
            while (parts.length) {
                i++;
                if (i>=this.params.length) {
                    break;
                }
                part = parts.shift().trim();
                if (!part)
                    continue;
                if (this.named_param_pattern.test(part)) {
                    parts.unshift(part);
                    break;
                }
                opts[this.params[i].name] = part.trim();
            }

            // Handle all named parameters
            for (i=0; i<parts.length; i++) {
                matches = parts[i].match(this.named_param_pattern);
                if (!matches) {
                    warn("Positional parameters not allowed after named parameters");
                    break;
                }
                if (this.named_params[matches[1]]===undefined) {
                    warn("Unknown named parameter " + matches[1]);
                    continue;
                }
                opts[matches[1]] = matches[2].trim();
            }

            return opts;
        }
    };

    return ArgumentParser;

});

/* jslint devel: true, browser: true, continue: true, sloppy: true, white: true, plusplus: true, regexp: true, indent: 8
 */
<commit_msg>simplified param storage, add_spec in dedicated function<commit_after>/**
 * @license
 * Patterns @VERSION@ parser - argument parser
 *
 * Copyright 2012 Simplon B.V.
 * Copyright 2012 Florian Friesdorf
 */
define([], function() {

    // older browsers - factor out if more need it
    // source: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/Trim
    if (!String.prototype.trim) {
        String.prototype.trim = function () {
            return this.replace(/^\s+|\s+$/g,'');
        };
    }

    // XXX: move somewhere
    // source: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/map
    // Production steps of ECMA-262, Edition 5, 15.4.4.19
    // Reference: http://es5.github.com/#x15.4.4.19
    if (!Array.prototype.map) {
        Array.prototype.map = function(callback, thisArg) {

            var T, A, k;

            if (this == null) {
                throw new TypeError(" this is null or not defined");
            }

            // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
            var O = Object(this);

            // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
            // 3. Let len be ToUint32(lenValue).
            var len = O.length >>> 0;

            // 4. If IsCallable(callback) is false, throw a TypeError exception.
            // See: http://es5.github.com/#x9.11
            if ({}.toString.call(callback) != "[object Function]") {
                throw new TypeError(callback + " is not a function");
            }

            // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
            if (thisArg) {
                T = thisArg;
            }

            // 6. Let A be a new array created as if by the expression new Array(len) where Array is
            // the standard built-in constructor with that name and len is the value of len.
            A = new Array(len);

            // 7. Let k be 0
            k = 0;

            // 8. Repeat, while k < len
            while(k < len) {

                var kValue, mappedValue;

                // a. Let Pk be ToString(k).
                //   This is implicit for LHS operands of the in operator
                // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
                //   This step can be combined with c
                // c. If kPresent is true, then
                if (k in O) {

                    // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
                    kValue = O[ k ];

                    // ii. Let mappedValue be the result of calling the Call internal method of callback
                    // with T as the this value and argument list containing kValue, k, and O.
                    mappedValue = callback.call(T, kValue, k, O);

                    // iii. Call the DefineOwnProperty internal method of A with arguments
                    // Pk, Property Descriptor {Value: mappedValue, Writable: true, Enumerable: true, Configurable: true},
                    // and false.

                    // In browsers that support Object.defineProperty, use the following:
                    // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });

                    // For best browser support, use the following:
                    A[ k ] = mappedValue;
                }
                // d. Increase k by 1.
                k++;
            }

            // 9. return A
            return A;
        };
    }

    // XXX: move to dedicated module
    var warn = function(msg) {
        try {
            console.warn(msg);
        } catch(e) {
        }
    };

    function ArgumentParser(spec) {
        this.params = [];
        this.defaults = {};
        if (spec) this.add_spec(spec);
    };
    ArgumentParser.prototype = {
        named_param_pattern: /^\s*([a-zA-z]+)\s*:(.*)/,

        add_argument: function(name, default_value) {
            if (default_value === undefined) default_value = null;
            this.params.push(name);
            this.defaults[name] = default_value;
        },

        add_spec: function(spec) {
            var parts = spec.split(';'),
                parser = this;
            for (var i=0, part; i<parts.length; i++) {
                part = parts[i].trim();
                var match = part.match(parser.named_param_pattern);
                if (match) {
                    parser.add_argument(match[1], match[2].trim());
                } else {
                    parser.add_argument(part);
                }
            }
        },

        parse: function(parameter) {
            if (parameter.match(/&&/)) {
                return parameter.split(/\s*&&\s*/).map(this.parse, this);
            }

            var parts = parameter.split(";"),
                opts = {},
                part, matches, i, name;

            // Popuplate options with default values
            for (i=0; i<this.params.length; i++) {
                name = this.params[i];
                opts[name] = this.defaults[name];
            }

            // Grab all positional parameters
            i=-1;
            while (parts.length) {
                i++;
                if (i>=this.params.length) {
                    break;
                }
                part = parts.shift().trim();
                if (!part)
                    continue;
                if (this.named_param_pattern.test(part)) {
                    parts.unshift(part);
                    break;
                }
                opts[this.params[i]] = part.trim();
            }

            // Handle all named parameters
            for (i=0; i<parts.length; i++) {
                matches = parts[i].match(this.named_param_pattern);
                if (!matches) {
                    warn("Positional parameters not allowed after named parameters");
                    break;
                }
                if (this.defaults[matches[1]] === undefined) {
                    warn("Unknown named parameter " + matches[1]);
                    continue;
                }
                opts[matches[1]] = matches[2].trim();
            }

            return opts;
        }
    };

    return ArgumentParser;

});

/* jslint devel: true, browser: true, continue: true, sloppy: true, white: true, plusplus: true, regexp: true, indent: 8
 */
<|endoftext|>


<commit_before>/**
 * Copyright 2015 CANAL+ Group
 *
 * 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.
 */

var Promise_ = require("canal-js-utils/promise");

var _ = require("canal-js-utils/misc");
var log = require("canal-js-utils/log");
var { CompositeDisposable, BehaviorSubject, Observable, Subject } = require("canal-js-utils/rx");
var { combineLatest, defer, zip } = Observable;
var { on } = require("canal-js-utils/rx-ext");
var EventEmitter = require("canal-js-utils/eventemitter");
var debugPane = require("../utils/debug");
var assert = require("canal-js-utils/assert");

var {
  HTMLVideoElement_,
  exitFullscreen,
  requestFullscreen,
  isFullscreen,
  onFullscreenChange
} = require("./compat");

var { timingsSampler, toWallClockTime, fromWallClockTime, getLiveGap } = require("./timings");
var { InitializationSegmentCache } = require("./cache");
var { getSize, getLoaded } = require("./ranges");
var { parseTimeFragment } = require("./time-fragment");
var DeviceEvents = require("./device-events");
var manifestHelpers = require("./manifest");
// TODO(pierre): separate transports from main build
var Transports = require("../net");
var PipeLines = require("./pipelines");
var Adaptive = require("../adaptive");
var Stream = require("./stream");
var EME = require("./eme");

var PLAYER_STOPPED   = "STOPPED";
var PLAYER_LOADED    = "LOADED";
var PLAYER_LOADING   = "LOADING";
var PLAYER_PLAYING   = "PLAYING";
var PLAYER_PAUSED    = "PAUSED";
var PLAYER_ENDED     = "ENDED";
var PLAYER_BUFFERING = "BUFFERING";
var PLAYER_SEEKING   = "SEEKING";

function createDirectFileManifest() {
  return {
    isLive: false,
    duration: Infinity,
  };
}

function assertMan(player) {
  assert(player.man, "player: no manifest loaded");
}

function filterStreamByType(stream, type) {
  return stream.filter(o => o.type == type).pluck("value");
}

class Player extends EventEmitter {

  constructor(options) {
    var {
      videoElement,
      transport,
      transportOptions,
      initVideoBitrate,
      initAudioBitrate
    } = options;

    super();
    this.defaultTransport = transport;
    this.defaultTransportOptions = transportOptions || {};

    if (!videoElement)
      videoElement = document.createElement("video");

    assert((videoElement instanceof HTMLVideoElement_),
      "requires an actual HTMLVideoElement");

    this.version = __RX_PLAYER_VERSION_PLACEHOLDER__;
    this.video = videoElement;

    // fullscreen change
    this.fullscreen = onFullscreenChange(videoElement)
      .subscribe(() => this.trigger("fullscreenChange", this.isFullscreen()));

    // playing state change
    this.playing = new BehaviorSubject();

    // multicaster forwarding all streams events
    this.stream = new Subject();

    var { createPipelines, metrics } = PipeLines();

    var timings = timingsSampler(videoElement);
    var deviceEvents = DeviceEvents(videoElement);

    this.createPipelines = createPipelines;
    this.metrics = metrics;
    this.timings = timings;

    this.adaptive = Adaptive(metrics, timings, deviceEvents, {
      initVideoBitrate,
      initAudioBitrate
    });

    // volume muted memory
    this.muted = 0.1;

    // states
    this._setState(PLAYER_STOPPED);
    this.resetStates();

    this.log = log;
  }

  resetStates() {
    this.man = null;
    this.reps = { video: null, audio: null, text: null };
    this.adas = { video: null, audio: null, text: null };
    this.evts = {};
    this.frag = { start: null, end: null };
  }

  _clear() {
    if (this.subscriptions) {
      this.subscriptions.dispose();
      this.subscriptions = null;
    }
  }

  stop() {
    if (this.state !== PLAYER_STOPPED) {
      this.resetStates();
      this._clear();
      this._setState(PLAYER_STOPPED);
    }
  }

  dispose() {
    this.stop();
    this.metrics.dispose();
    this.adaptive.dispose();
    this.fullscreen.dispose();
    this.stream.dispose();

    this.metrics = null;
    this.adaptive = null;
    this.fullscreen = null;
    this.stream = null;

    this.timings = null;
    this.createPipelines = null;
    this.video = null;
  }

  __recordState(type, value) {
    var prev = this.evts[type];
    if (prev !== value) {
      this.evts[type] = value;
      this.trigger(`${type}Change`, value);
    }
  }

  _parseOptions(opts) {
    var {
      transport,
      transportOptions,
      url,
      keySystems,
      timeFragment,
      subtitles,
      autoPlay,
      directFile
    } = _.defaults(_.cloneObject(opts), {
      transport: this.defaultTransport,
      transportOptions: {},
      keySystems: [],
      timeFragment: {},
      subtitles: [],
      autoPlay: false,
      directFile: false,
    });

    timeFragment = parseTimeFragment(timeFragment);

    if (_.isString(transport))
      transport = Transports[transport];

    if (_.isFunction(transport))
      transport = transport(_.defaults(transportOptions, this.defaultTransportOptions));

    assert(transport, "player: transport " + opts.transport + " is not supported");

    if (directFile)
      directFile = createDirectFileManifest();

    return { url, keySystems, subtitles, timeFragment, autoPlay, transport, directFile };
  }

  loadVideo(options = {}) {
    options = this._parseOptions(options);
    log.info("loadvideo", options);

    var {
      url,
      keySystems,
      subtitles,
      timeFragment,
      autoPlay,
      transport,
      directFile
    } = options;

    this.stop();
    this.frag = timeFragment;
    this.playing.onNext(autoPlay);

    var pipelines = this.createPipelines(transport, {
      segment: {
        cache: InitializationSegmentCache()
      }
    });

    var { adaptive, timings, video } = this;
    var stream;
    try {
      stream = Stream({
        url,
        keySystems,
        subtitles,
        timings,
        timeFragment,
        adaptive,
        pipelines,
        videoElement: video,
        autoPlay,
        directFile,
      });
    }
    catch(err) {
      stream = defer(() => { throw err; });
    }

    stream = stream.publish();

    var segments = filterStreamByType(stream, "segment");
    var manifests = filterStreamByType(stream, "manifest");

    var stalled = filterStreamByType(stream, "stalled").startWith(null);
    var canPlay = filterStreamByType(stream, "loaded").filter(v => v === true);

    var loaded;

    if (directFile) {
      loaded = canPlay;
    } else {
      loaded = zip(
        canPlay,
        filterStreamByType(segments.pluck("adaptation"), "audio"),
        filterStreamByType(segments.pluck("adaptation"), "video"),
        _.noop);
    }

    loaded = loaded.take(1);

    var stateChanges = loaded.map(PLAYER_LOADED)
      .concat(combineLatest(this.playing, stalled,
        (isPlaying, isStalled) => {
          if (isStalled)
            return (isStalled.name == "seeking")
              ? PLAYER_SEEKING
              : PLAYER_BUFFERING;

          if (isPlaying)
            return PLAYER_PLAYING;

          return PLAYER_PAUSED;
        })
      )
      .changes()
      .startWith(PLAYER_LOADING);

    this.subscriptions = new CompositeDisposable();
    var subs = [
      on(video, ["play", "pause"])
        .each(evt => this.playing.onNext(evt.type == "play")),

      segments.each((segment) => {
        var type = segment.adaptation.type;

        var rep = segment.representation;
        var ada = segment.adaptation;
        this.reps[type] = rep;
        this.adas[type] = ada;

        if (type == "text") {
          this.__recordState("subtitle", ada.lang);
        }
        if (type == "video") {
          this.__recordState("videoBitrate", rep.bitrate);
        }
        if (type == "audio") {
          this.__recordState("language", ada.lang);
          this.__recordState("audioBitrate", rep.bitrate);
        }

        this.trigger("progress", segment);
      }),

      manifests.each(m => {
        this.man = m;
        this.trigger("manifestChange", m);
      }),

      stateChanges
        .each(s => this._setState(s)),

      timings
        .each(t => {
          if (!this.man)
            return;

          if (this.man.isLive && t.ts > 0) {
            t.wallClockTime = toWallClockTime(t.ts, this.man);
            t.liveGap = getLiveGap(t.ts, this.man);
          }

          this.trigger("currentTimeChange", t);
        }),

      stream.subscribe(
        () => {},
        (e) => {
          this.resetStates();
          this.trigger("error", e);
          this._setState(PLAYER_STOPPED);
          this._clear();
        },
        () => {
          this.resetStates();
          this._setState(PLAYER_ENDED);
          this._clear();
        }
      ),

      stream.subscribe(
        n => this.stream.onNext(n),
        e => this.stream.onNext({ type: "error", value: e })
      ),

      stream.connect()
    ];

    _.each(subs, s => this.subscriptions.add(s));

    return loaded.toPromise();
  }

  _setState(s) {
    if (this.state !== s) {
      this.state = s;
      this.trigger("playerStateChange", s);
    }
  }

  getManifest() {
    return this.man;
  }

  getVideoElement() {
    return this.video;
  }

  getNativeTextTrack() {
    return this.video.textTracks[0];
  }

  getPlayerState() {
    return this.state;
  }

  isLive() {
    assertMan(this);
    return this.man.isLive;
  }

  getUrl() {
    assertMan(this);
    return this.man.baseURL;
  }

  getVideoDuration() {
    return this.video.duration;
  }

  getVideoLoadedTime() {
    return getSize(this.video.currentTime, this.video.buffered);
  }

  getVideoPlayedTime() {
    return getLoaded(this.video.currentTime, this.video.buffered);
  }

  getCurrentTime() {
    if (!this.man)
      return NaN;
    var ct = this.video.currentTime;
    if (this.man.isLive) {
      return toWallClockTime(ct, this.man);
    } else {
      return ct;
    }
  }

  getStartTime() {
    return this.frag.start;
  }

  getEndTime() {
    return this.frag.end;
  }

  getPlaybackRate() {
    return this.video.playbackRate;
  }

  getVolume() {
    return this.video.volume;
  }

  isFullscreen() {
    return isFullscreen();
  }

  getAvailableLanguages() {
    return this.man && manifestHelpers.getAvailableLanguages(this.man) || [];
  }

  getAvailableSubtitles() {
    return this.man && manifestHelpers.getAvailableSubtitles(this.man) || [];
  }

  getLanguage() {
    return this.adaptive.getLanguage();
  }

  getSubtitle() {
    return this.adaptive.getSubtitle();
  }

  getAvailableVideoBitrates() {
    var video = this.man && this.man.adaptations.video[0];
    return (video && video.bitrates) || [];
  }

  getAvailableAudioBitrates() {
    var audio = this.adas.audio;
    return (audio && audio.bitrates) || [];
  }

  getVideoBitrate() {
    return this.evts.videoBitrate;
  }

  getAudioBitrate() {
    return this.evts.audioBitrate;
  }

  getVideoMaxBitrate() {
    return this.adaptive.getVideoMaxBitrate();
  }

  getAudioMaxBitrate() {
    return this.adaptive.getAudioMaxBitrate();
  }

  getVideoBufferSize() {
    return this.adaptive.getVideoBufferSize();
  }

  getAudioBufferSize() {
    return this.adaptive.getAudioBufferSize();
  }

  getAverageBitrates() {
    return this.adaptive.getAverageBitrates();
  }

  getMetrics() {
    return this.metrics;
  }

  getTimings() {
    return this.timings;
  }

  play() {
    this.video.play();
  }

  pause() {
    this.video.pause();
  }

  setPlaybackRate(rate) {
    return new Promise_(res => res(this.video.playbackRate = rate));
  }

  goToStart() {
    return this.seekTo(this.getStartTime());
  }

  seekTo(time) {
    return new Promise_(res => {
      assert(this.man);
      var currentTs = this.video.currentTime;
      if (this.man.isLive) time = fromWallClockTime(time, this.man);
      if (time !== currentTs) {
        log.info("seek to", time);
        res(this.video.currentTime = time);
      } else {
        res(currentTs);
      }
    });
  }

  setFullscreen(toggle = true) {
    if (toggle === false)
      exitFullscreen();
    else
      requestFullscreen(this.video);
  }

  setVolume(volume) {
    if (volume !== this.video.volume) {
      this.video.volume = volume;
      this.trigger("volumeChange", volume);
    }
  }

  mute() {
    this.muted = this.getVolume() || 0.1;
    this.setVolume(0);
  }

  unMute() {
    var vol = this.getVolume();
    if (vol === 0) this.setVolume(this.muted);
  }

  setLanguage(lng) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(_.contains(this.getAvailableLanguages(), lng), "player: unknown language");
      res(this.adaptive.setLanguage(lng));
    });
  }

  setSubtitle(sub) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(!sub || _.contains(this.getAvailableSubtitles(), sub), "player: unknown subtitle");
      res(this.adaptive.setSubtitle(sub || ""));
    })
      .then(() => {
        if (!sub)
          this.__recordState("subtitle", null);
      });
  }

  setVideoBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(btr === 0 || _.contains(this.getAvailableVideoBitrates(), btr), "player: video bitrate unavailable");
      res(this.adaptive.setVideoBitrate(btr));
    });
  }

  setAudioBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(btr === 0 || _.contains(this.getAvailableAudioBitrates(), btr), "player: audio bitrate unavailable");
      res(this.adaptive.setAudioBitrate(btr));
    });
  }

  setVideoMaxBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      res(this.adaptive.setVideoMaxBitrate(btr));
    });
  }

  setAudioMaxBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      res(this.adaptive.setAudioMaxBitrate(btr));
    });
  }

  setVideoBufferSize(size) {
    // TODO(pierre): proper promise
    return new Promise_(res => res(this.adaptive.setVideoBufferSize(size)));
  }

  setAudioBufferSize(size) {
    // TODO(pierre): proper promise
    return new Promise_(res => res(this.adaptive.setAudioBufferSize(size)));
  }

  getStreamObservable() {
    return this.stream;
  }

  getDebug() {
    return debugPane.getDebug(this);
  }

  showDebug() {
    debugPane.showDebug(this, this.video);
  }

  hideDebug() {
    debugPane.hideDebug();
  }

  toggleDebug() {
    debugPane.toggleDebug(this,this.video);
  }
  getCurrentKeySystem() {
    return EME.getCurrentKeySystem();
  }
}

module.exports = Player;
<commit_msg>player: fix getUrl<commit_after>/**
 * Copyright 2015 CANAL+ Group
 *
 * 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.
 */

var Promise_ = require("canal-js-utils/promise");

var _ = require("canal-js-utils/misc");
var log = require("canal-js-utils/log");
var { CompositeDisposable, BehaviorSubject, Observable, Subject } = require("canal-js-utils/rx");
var { combineLatest, defer, zip } = Observable;
var { on } = require("canal-js-utils/rx-ext");
var EventEmitter = require("canal-js-utils/eventemitter");
var debugPane = require("../utils/debug");
var assert = require("canal-js-utils/assert");

var {
  HTMLVideoElement_,
  exitFullscreen,
  requestFullscreen,
  isFullscreen,
  onFullscreenChange
} = require("./compat");

var { timingsSampler, toWallClockTime, fromWallClockTime, getLiveGap } = require("./timings");
var { InitializationSegmentCache } = require("./cache");
var { getSize, getLoaded } = require("./ranges");
var { parseTimeFragment } = require("./time-fragment");
var DeviceEvents = require("./device-events");
var manifestHelpers = require("./manifest");
// TODO(pierre): separate transports from main build
var Transports = require("../net");
var PipeLines = require("./pipelines");
var Adaptive = require("../adaptive");
var Stream = require("./stream");
var EME = require("./eme");

var PLAYER_STOPPED   = "STOPPED";
var PLAYER_LOADED    = "LOADED";
var PLAYER_LOADING   = "LOADING";
var PLAYER_PLAYING   = "PLAYING";
var PLAYER_PAUSED    = "PAUSED";
var PLAYER_ENDED     = "ENDED";
var PLAYER_BUFFERING = "BUFFERING";
var PLAYER_SEEKING   = "SEEKING";

function createDirectFileManifest() {
  return {
    isLive: false,
    duration: Infinity,
  };
}

function assertMan(player) {
  assert(player.man, "player: no manifest loaded");
}

function filterStreamByType(stream, type) {
  return stream.filter(o => o.type == type).pluck("value");
}

class Player extends EventEmitter {

  constructor(options) {
    var {
      videoElement,
      transport,
      transportOptions,
      initVideoBitrate,
      initAudioBitrate
    } = options;

    super();
    this.defaultTransport = transport;
    this.defaultTransportOptions = transportOptions || {};

    if (!videoElement)
      videoElement = document.createElement("video");

    assert((videoElement instanceof HTMLVideoElement_),
      "requires an actual HTMLVideoElement");

    this.version = __RX_PLAYER_VERSION_PLACEHOLDER__;
    this.video = videoElement;

    // fullscreen change
    this.fullscreen = onFullscreenChange(videoElement)
      .subscribe(() => this.trigger("fullscreenChange", this.isFullscreen()));

    // playing state change
    this.playing = new BehaviorSubject();

    // multicaster forwarding all streams events
    this.stream = new Subject();

    var { createPipelines, metrics } = PipeLines();

    var timings = timingsSampler(videoElement);
    var deviceEvents = DeviceEvents(videoElement);

    this.createPipelines = createPipelines;
    this.metrics = metrics;
    this.timings = timings;

    this.adaptive = Adaptive(metrics, timings, deviceEvents, {
      initVideoBitrate,
      initAudioBitrate
    });

    // volume muted memory
    this.muted = 0.1;

    // states
    this._setState(PLAYER_STOPPED);
    this.resetStates();

    this.log = log;
  }

  resetStates() {
    this.man = null;
    this.reps = { video: null, audio: null, text: null };
    this.adas = { video: null, audio: null, text: null };
    this.evts = {};
    this.frag = { start: null, end: null };
  }

  _clear() {
    if (this.subscriptions) {
      this.subscriptions.dispose();
      this.subscriptions = null;
    }
  }

  stop() {
    if (this.state !== PLAYER_STOPPED) {
      this.resetStates();
      this._clear();
      this._setState(PLAYER_STOPPED);
    }
  }

  dispose() {
    this.stop();
    this.metrics.dispose();
    this.adaptive.dispose();
    this.fullscreen.dispose();
    this.stream.dispose();

    this.metrics = null;
    this.adaptive = null;
    this.fullscreen = null;
    this.stream = null;

    this.timings = null;
    this.createPipelines = null;
    this.video = null;
  }

  __recordState(type, value) {
    var prev = this.evts[type];
    if (prev !== value) {
      this.evts[type] = value;
      this.trigger(`${type}Change`, value);
    }
  }

  _parseOptions(opts) {
    var {
      transport,
      transportOptions,
      url,
      keySystems,
      timeFragment,
      subtitles,
      autoPlay,
      directFile
    } = _.defaults(_.cloneObject(opts), {
      transport: this.defaultTransport,
      transportOptions: {},
      keySystems: [],
      timeFragment: {},
      subtitles: [],
      autoPlay: false,
      directFile: false,
    });

    timeFragment = parseTimeFragment(timeFragment);

    if (_.isString(transport))
      transport = Transports[transport];

    if (_.isFunction(transport))
      transport = transport(_.defaults(transportOptions, this.defaultTransportOptions));

    assert(transport, "player: transport " + opts.transport + " is not supported");

    if (directFile)
      directFile = createDirectFileManifest();

    return { url, keySystems, subtitles, timeFragment, autoPlay, transport, directFile };
  }

  loadVideo(options = {}) {
    options = this._parseOptions(options);
    log.info("loadvideo", options);

    var {
      url,
      keySystems,
      subtitles,
      timeFragment,
      autoPlay,
      transport,
      directFile
    } = options;

    this.stop();
    this.frag = timeFragment;
    this.playing.onNext(autoPlay);

    var pipelines = this.createPipelines(transport, {
      segment: {
        cache: InitializationSegmentCache()
      }
    });

    var { adaptive, timings, video } = this;
    var stream;
    try {
      stream = Stream({
        url,
        keySystems,
        subtitles,
        timings,
        timeFragment,
        adaptive,
        pipelines,
        videoElement: video,
        autoPlay,
        directFile,
      });
    }
    catch(err) {
      stream = defer(() => { throw err; });
    }

    stream = stream.publish();

    var segments = filterStreamByType(stream, "segment");
    var manifests = filterStreamByType(stream, "manifest");

    var stalled = filterStreamByType(stream, "stalled").startWith(null);
    var canPlay = filterStreamByType(stream, "loaded").filter(v => v === true);

    var loaded;

    if (directFile) {
      loaded = canPlay;
    } else {
      loaded = zip(
        canPlay,
        filterStreamByType(segments.pluck("adaptation"), "audio"),
        filterStreamByType(segments.pluck("adaptation"), "video"),
        _.noop);
    }

    loaded = loaded.take(1);

    var stateChanges = loaded.map(PLAYER_LOADED)
      .concat(combineLatest(this.playing, stalled,
        (isPlaying, isStalled) => {
          if (isStalled)
            return (isStalled.name == "seeking")
              ? PLAYER_SEEKING
              : PLAYER_BUFFERING;

          if (isPlaying)
            return PLAYER_PLAYING;

          return PLAYER_PAUSED;
        })
      )
      .changes()
      .startWith(PLAYER_LOADING);

    this.subscriptions = new CompositeDisposable();
    var subs = [
      on(video, ["play", "pause"])
        .each(evt => this.playing.onNext(evt.type == "play")),

      segments.each((segment) => {
        var type = segment.adaptation.type;

        var rep = segment.representation;
        var ada = segment.adaptation;
        this.reps[type] = rep;
        this.adas[type] = ada;

        if (type == "text") {
          this.__recordState("subtitle", ada.lang);
        }
        if (type == "video") {
          this.__recordState("videoBitrate", rep.bitrate);
        }
        if (type == "audio") {
          this.__recordState("language", ada.lang);
          this.__recordState("audioBitrate", rep.bitrate);
        }

        this.trigger("progress", segment);
      }),

      manifests.each(m => {
        this.man = m;
        this.trigger("manifestChange", m);
      }),

      stateChanges
        .each(s => this._setState(s)),

      timings
        .each(t => {
          if (!this.man)
            return;

          if (this.man.isLive && t.ts > 0) {
            t.wallClockTime = toWallClockTime(t.ts, this.man);
            t.liveGap = getLiveGap(t.ts, this.man);
          }

          this.trigger("currentTimeChange", t);
        }),

      stream.subscribe(
        () => {},
        (e) => {
          this.resetStates();
          this.trigger("error", e);
          this._setState(PLAYER_STOPPED);
          this._clear();
        },
        () => {
          this.resetStates();
          this._setState(PLAYER_ENDED);
          this._clear();
        }
      ),

      stream.subscribe(
        n => this.stream.onNext(n),
        e => this.stream.onNext({ type: "error", value: e })
      ),

      stream.connect()
    ];

    _.each(subs, s => this.subscriptions.add(s));

    return loaded.toPromise();
  }

  _setState(s) {
    if (this.state !== s) {
      this.state = s;
      this.trigger("playerStateChange", s);
    }
  }

  getManifest() {
    return this.man;
  }

  getVideoElement() {
    return this.video;
  }

  getNativeTextTrack() {
    return this.video.textTracks[0];
  }

  getPlayerState() {
    return this.state;
  }

  isLive() {
    assertMan(this);
    return this.man.isLive;
  }

  getUrl() {
    assertMan(this);
    return this.man.locations[0];
  }

  getVideoDuration() {
    return this.video.duration;
  }

  getVideoLoadedTime() {
    return getSize(this.video.currentTime, this.video.buffered);
  }

  getVideoPlayedTime() {
    return getLoaded(this.video.currentTime, this.video.buffered);
  }

  getCurrentTime() {
    if (!this.man)
      return NaN;
    var ct = this.video.currentTime;
    if (this.man.isLive) {
      return toWallClockTime(ct, this.man);
    } else {
      return ct;
    }
  }

  getStartTime() {
    return this.frag.start;
  }

  getEndTime() {
    return this.frag.end;
  }

  getPlaybackRate() {
    return this.video.playbackRate;
  }

  getVolume() {
    return this.video.volume;
  }

  isFullscreen() {
    return isFullscreen();
  }

  getAvailableLanguages() {
    return this.man && manifestHelpers.getAvailableLanguages(this.man) || [];
  }

  getAvailableSubtitles() {
    return this.man && manifestHelpers.getAvailableSubtitles(this.man) || [];
  }

  getLanguage() {
    return this.adaptive.getLanguage();
  }

  getSubtitle() {
    return this.adaptive.getSubtitle();
  }

  getAvailableVideoBitrates() {
    var video = this.man && this.man.adaptations.video[0];
    return (video && video.bitrates) || [];
  }

  getAvailableAudioBitrates() {
    var audio = this.adas.audio;
    return (audio && audio.bitrates) || [];
  }

  getVideoBitrate() {
    return this.evts.videoBitrate;
  }

  getAudioBitrate() {
    return this.evts.audioBitrate;
  }

  getVideoMaxBitrate() {
    return this.adaptive.getVideoMaxBitrate();
  }

  getAudioMaxBitrate() {
    return this.adaptive.getAudioMaxBitrate();
  }

  getVideoBufferSize() {
    return this.adaptive.getVideoBufferSize();
  }

  getAudioBufferSize() {
    return this.adaptive.getAudioBufferSize();
  }

  getAverageBitrates() {
    return this.adaptive.getAverageBitrates();
  }

  getMetrics() {
    return this.metrics;
  }

  getTimings() {
    return this.timings;
  }

  play() {
    this.video.play();
  }

  pause() {
    this.video.pause();
  }

  setPlaybackRate(rate) {
    return new Promise_(res => res(this.video.playbackRate = rate));
  }

  goToStart() {
    return this.seekTo(this.getStartTime());
  }

  seekTo(time) {
    return new Promise_(res => {
      assert(this.man);
      var currentTs = this.video.currentTime;
      if (this.man.isLive) time = fromWallClockTime(time, this.man);
      if (time !== currentTs) {
        log.info("seek to", time);
        res(this.video.currentTime = time);
      } else {
        res(currentTs);
      }
    });
  }

  setFullscreen(toggle = true) {
    if (toggle === false)
      exitFullscreen();
    else
      requestFullscreen(this.video);
  }

  setVolume(volume) {
    if (volume !== this.video.volume) {
      this.video.volume = volume;
      this.trigger("volumeChange", volume);
    }
  }

  mute() {
    this.muted = this.getVolume() || 0.1;
    this.setVolume(0);
  }

  unMute() {
    var vol = this.getVolume();
    if (vol === 0) this.setVolume(this.muted);
  }

  setLanguage(lng) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(_.contains(this.getAvailableLanguages(), lng), "player: unknown language");
      res(this.adaptive.setLanguage(lng));
    });
  }

  setSubtitle(sub) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(!sub || _.contains(this.getAvailableSubtitles(), sub), "player: unknown subtitle");
      res(this.adaptive.setSubtitle(sub || ""));
    })
      .then(() => {
        if (!sub)
          this.__recordState("subtitle", null);
      });
  }

  setVideoBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(btr === 0 || _.contains(this.getAvailableVideoBitrates(), btr), "player: video bitrate unavailable");
      res(this.adaptive.setVideoBitrate(btr));
    });
  }

  setAudioBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      assert(btr === 0 || _.contains(this.getAvailableAudioBitrates(), btr), "player: audio bitrate unavailable");
      res(this.adaptive.setAudioBitrate(btr));
    });
  }

  setVideoMaxBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      res(this.adaptive.setVideoMaxBitrate(btr));
    });
  }

  setAudioMaxBitrate(btr) {
    // TODO(pierre): proper promise
    return new Promise_(res => {
      res(this.adaptive.setAudioMaxBitrate(btr));
    });
  }

  setVideoBufferSize(size) {
    // TODO(pierre): proper promise
    return new Promise_(res => res(this.adaptive.setVideoBufferSize(size)));
  }

  setAudioBufferSize(size) {
    // TODO(pierre): proper promise
    return new Promise_(res => res(this.adaptive.setAudioBufferSize(size)));
  }

  getStreamObservable() {
    return this.stream;
  }

  getDebug() {
    return debugPane.getDebug(this);
  }

  showDebug() {
    debugPane.showDebug(this, this.video);
  }

  hideDebug() {
    debugPane.hideDebug();
  }

  toggleDebug() {
    debugPane.toggleDebug(this,this.video);
  }
  getCurrentKeySystem() {
    return EME.getCurrentKeySystem();
  }
}

module.exports = Player;
<|endoftext|>