<commit_before>/*jslint white:false plusplus:false browser:true nomen:false */
/*globals window */
/* FuncyTag version 0.0.4 - see more at https://github.com/BrentNoorda/FuncyTag */

(function(window) {
    var gCamelToDashRegexp, gNextUniqueId, gNextIdChar;

    /*************** build tag ***************/

    gCamelToDashRegexp = new RegExp('([A-Z])','g');

    function camel_to_dash(camel)
    // turn upper characters to -c, for example, "cssBackgroundColor" becomes "css-background-color"
    {
        return camel.replace(gCamelToDashRegexp,function(match,c,offset,string){
                //alert("arguments.length = " + arguments.length + ' match='+match + ' c='+c+' offset='+offset+' string='+string);
                return '-' + c.toLowerCase();
            });
    }

    /*************** utility functions ***************/

    function is_array(o) {  // from http://web.archive.org/web/20100424091244/http://www.ajaxdr.com/code/javascript-version-of-phps-is_array-function/
        if((o !== null) && (typeof o === 'object')) {
            return (typeof o.push === 'undefined') ? false : true;
        }else {
            return false;
        }
    }

    function isPlainObj(o) { // from http://stackoverflow.com/questions/5876332/how-can-i-differentiate-between-an-object-literal-other-javascript-objects
        return (typeof o === 'object') && (o.constructor === Object)  && !('_ft_render_' in o);
    }

    /*************** global event trackers ***************/

    var gNextUniqueId = "";  // each time an id isn't supplied, and we need one, supply it here
    var gUniqueLen = 0;
    var gNextIdChar = 'a'; // will roll among all the characters of the alphabet

    function get_next_unique_id() {
        var ret = gNextUniqueId + gNextIdChar + '-ft-unq';

        function increment_root_string(len)
        {
            var ret = true, endChar;
            if ( len === 0 )
                ret = false;
            else
            {
                endChar = gNextUniqueId.charAt(len-1);
                if ( endChar === 'z' )
                {
                    endChar = 'a';
                    ret = increment_root_string(len-1);
                }
                else
                {
                    endChar = String.fromCharCode(endChar.charCodeAt(0)+1);
                }
                gNextUniqueId = gNextUniqueId.substring(0,len-1) + endChar;
            }
            return ret;
        }

        if ( gNextIdChar === 'z' )
        {
            // increment the root string
            if ( !increment_root_string(gUniqueLen) )
            {
                gUniqueLen++;
                gNextUniqueId += 'a';
            }
            gNextIdChar = 'a';
        }
        else
        {
            gNextIdChar = String.fromCharCode(gNextIdChar.charCodeAt(0)+1);
        }
        return ret;
    }

    /*************** build tag ***************/

    function funcyTag(name_or_funcytag,default_attributes)
    {
        var name, tag, prop;

        try
        {
            if ( '_ft_name' in name_or_funcytag )
            {
                // if name_or_functytag is itself another funcytag, then inherit default attributes from that one
                name = name_or_funcytag._ft_name;
                if ( name_or_funcytag.default_attributes !== undefined )
                {
                    default_attributes = default_attributes || { };
                    /*jslint forin:true */
                    for ( prop in name_or_funcytag.default_attributes )
                    {
                        if ( !(prop in default_attributes) )
                        {
                            default_attributes[prop] = name_or_funcytag.default_attributes[prop];
                        }
                    }
                    /*jslint forin:false */
                }
            }
        } catch(e) {
            name = name_or_funcytag;
        }

        /*************** tag instance ***************/

        tag = function(attributes/*,inside1,inside2...*/) {
            var tagobj, argidx, inside, xprop, has_attr;
            has_attr = isPlainObj(attributes);
            tagobj = { _ft_name:name, _insides:[], _attributes: has_attr ? attributes :  {} };

            if ( default_attributes !== undefined )
            {
                // inherit default attributes, but let attributes override them
                tagobj._attributes = { };
                /*jslint forin:true */
                for ( xprop in default_attributes )
                {
                    tagobj._attributes[xprop] = default_attributes[xprop];
                }
                if ( has_attr )
                {
                    for ( xprop in attributes )
                    {
                        tagobj._attributes[xprop] = attributes[xprop];
                    }
                }
                /*jslint forin:false */
            }
            for ( argidx = has_attr ? 1 : 0; argidx < arguments.length; argidx++ )
            {
                inside = arguments[argidx];
                tagobj._insides.push(inside);
            }

            tagobj._ft_render_ = function(stringPadding) {
                var html, idx, jdx, content, inside, value, attr, units, style, innerHTML, first_, innerPadding, skipNextPadding;

                // start tag and add all attributes, including decoding of styles
                style = '';
                html = '<' + this._ft_name;
                /*jslint forin:true */
                for ( attr in this._attributes )
                {
                    first_ = attr.indexOf('_'); // skip attributes that start with underscore
                    if ( first_ !== 0 )
                    {
                        value = this._attributes[attr];
                        if ( value !== undefined )  // if decodes as undefined then don't add this attribute
                        {
                            // sometimes there is a unit as part of the attr name
                            if ( -1 === first_ )
                            {
                                units = '';
                            }
                            else
                            {
                                jdx = attr.lastIndexOf('_');
                                units = attr.substring(jdx+1);
                                attr = attr.substring(0,jdx);
                                if ( units === 'pct') // because cannot have % be part of a name
                                {
                                    units = '%';
                                }
                            }

                            if ( is_array(value) )
                            {
                                // do not count any undefined values, so remove those from the array
                                for ( jdx = 0; jdx < value.length; jdx++ )
                                {
                                    if ( value[jdx] === undefined )
                                    {
                                        value.splice(jdx,1);
                                        jdx--;
                                    }
                                }
                                if ( value.length === 0 )
                                {
                                    // no values in the array, so don't output this attribute
                                    continue;
                                }
                                // if value is an array, turn it into a space-delimited string
                                value = value.join(units + ' ');
                            }
                            value += units;

                            if ( attr.substring(0,3) === 'css' )
                            {
                                style += camel_to_dash(attr).substring(4) + ':' + value + ';';
                            }
                            else
                            {
                                html += ' ' + attr + '="' + value + '"';
                            }
                        }
                    }
                }
                /*jslint forin:false */

                if ( !this._attributes._nobrout )
                {
                    html = stringPadding + html;
                }

                if ( (stringPadding.length === 0) && !this._attributes._nobrin )
                {
                    stringPadding = '\r\n';
                }

                if ( style.length !== 0 )
                {
                    html += ' style="' + style + '"';
                }

                innerHTML = '';
                innerPadding = this._attributes._nobrin ? '' : (stringPadding + '  ');
                skipNextPadding = false;
                for ( idx = 0; idx < this._insides.length; idx++ )
                {
                    inside = this._insides[idx];
                    if ( !is_array(inside) ) { inside = [inside]; }
                    for ( jdx = 0; jdx < inside.length; jdx++ )
                    {
                        content = inside[jdx];
                        if ( content !== undefined )
                        {
                            try {
                                innerHTML += content._ft_render_(skipNextPadding?'':innerPadding);
                                skipNextPadding = content._attributes._nobrout;
                            } catch(e) {
                                if ( !skipNextPadding )
                                {
                                    innerHTML += innerPadding;
                                }
                                else
                                {
                                    skipNextPadding = false;
                                }
                                innerHTML += String(content);
                            }
                        }
                    }
                }

                if ( (innerHTML.length === 0) && !(this._attributes._noslfcls) )
                {
                    html += '/>';
                }
                else
                {
                    html += '>' + innerHTML + ((this._attributes._nobrin) ? '' : stringPadding) + '</' + this._ft_name + '>';
                }
                return html;
            };

            tagobj.toString = function() { return this._ft_render_(''); };

            return tagobj;
        };
        tag._ft_name = name;
        tag.default_attributes = default_attributes;
        return tag;
    }

    /*************** initialize ***************/

    window.funcyTag = funcyTag;

    /*************** the "esc" feature ***************/

    window.funcyTag.esc = function( str )
    {
        return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
    };

}(window));
<commit_msg>lint checkpoint<commit_after>/*jslint white:false plusplus:false browser:true nomen:false */
/*globals window */
/* FuncyTag version 0.0.4 - see more at https://github.com/BrentNoorda/FuncyTag */

(function(window) {
    var gCamelToDashRegexp, gNextUniqueId, gUniqueLen, gNextIdChar, gInitQueue, gElems;

    /*************** build tag ***************/

    gCamelToDashRegexp = new RegExp('([A-Z])','g');

    function camel_to_dash(camel)
    // turn upper characters to -c, for example, "cssBackgroundColor" becomes "css-background-color"
    {
        return camel.replace(gCamelToDashRegexp,function(match,c,offset,string){
                //alert("arguments.length = " + arguments.length + ' match='+match + ' c='+c+' offset='+offset+' string='+string);
                return '-' + c.toLowerCase();
            });
    }

    /*************** utility functions ***************/

    function is_array(o) {  // from http://web.archive.org/web/20100424091244/http://www.ajaxdr.com/code/javascript-version-of-phps-is_array-function/
        if((o !== null) && (typeof o === 'object')) {
            return (typeof o.push === 'undefined') ? false : true;
        }else {
            return false;
        }
    }

    function isPlainObj(o) { // from http://stackoverflow.com/questions/5876332/how-can-i-differentiate-between-an-object-literal-other-javascript-objects
        return (typeof o === 'object') && (o.constructor === Object)  && !('_ft_render_' in o);
    }

    /*************** tool to create a unique ID, forever, if one is not suppliedglobal event trackers ***************/

    gNextUniqueId = "";  // each time an id isn't supplied, and we need one, supply it here
    gUniqueLen = 0;
    gNextIdChar = 'a'; // will roll among all the characters of the alphabet

    function get_next_unique_id() {
        var ret = gNextUniqueId + gNextIdChar + '-ft-unq';

        function increment_root_string(len)
        {
            var ret = true, endChar;
            if ( len === 0 )
            {
                ret = false;
            }
            else
            {
                endChar = gNextUniqueId.charAt(len-1);
                if ( endChar === 'z' )
                {
                    endChar = 'a';
                    ret = increment_root_string(len-1);
                }
                else
                {
                    endChar = String.fromCharCode(endChar.charCodeAt(0)+1);
                }
                gNextUniqueId = gNextUniqueId.substring(0,len-1) + endChar;
            }
            return ret;
        }

        if ( gNextIdChar === 'z' )
        {
            // increment the root string
            if ( !increment_root_string(gUniqueLen) )
            {
                gUniqueLen++;
                gNextUniqueId += 'a';
            }
            gNextIdChar = 'a';
        }
        else
        {
            gNextIdChar = String.fromCharCode(gNextIdChar.charCodeAt(0)+1);
        }
        return ret;
    }

    /*************** global event/data trackers ***************/
    gInitQueue = { }; // elements waiting to initialize (via onrender), indexed by element id
    gElems = { }; // all elements we keep data about (indexed by id)

    function debugTrackers() {
        var id, ret = "FuncyTag debug trackers";
        ret += "\r\n" + gInitQueue.length + " items in gInitQueue\r\n.";
        if ( gInitQueue.length !== 0 )
        {
            ret += "They are:\r\n";
            for ( id in gInitQueue )
            {
                if ( gInitQueue.hasOwnProperty(id) )
                {
                    ret += " " + id + "\r\n";
                }
            }
        }

    }

    /*************** build tag ***************/

    function funcyTag(name_or_funcytag,default_attributes)
    {
        var name, tag, prop;

        try
        {
            if ( '_ft_name' in name_or_funcytag )
            {
                // if name_or_functytag is itself another funcytag, then inherit default attributes from that one
                name = name_or_funcytag._ft_name;
                if ( name_or_funcytag.default_attributes !== undefined )
                {
                    default_attributes = default_attributes || { };
                    /*jslint forin:true */
                    for ( prop in name_or_funcytag.default_attributes )
                    {
                        if ( !(prop in default_attributes) )
                        {
                            default_attributes[prop] = name_or_funcytag.default_attributes[prop];
                        }
                    }
                    /*jslint forin:false */
                }
            }
        } catch(e) {
            name = name_or_funcytag;
        }

        /*************** tag instance ***************/

        tag = function(attributes/*,inside1,inside2...*/) {
            var tagobj, argidx, inside, xprop, has_attr;
            has_attr = isPlainObj(attributes);
            tagobj = { _ft_name:name, _insides:[], _attributes: has_attr ? attributes :  {} };

            if ( default_attributes !== undefined )
            {
                // inherit default attributes, but let attributes override them
                tagobj._attributes = { };
                /*jslint forin:true */
                for ( xprop in default_attributes )
                {
                    tagobj._attributes[xprop] = default_attributes[xprop];
                }
                if ( has_attr )
                {
                    for ( xprop in attributes )
                    {
                        tagobj._attributes[xprop] = attributes[xprop];
                    }
                }
                /*jslint forin:false */
            }
            for ( argidx = has_attr ? 1 : 0; argidx < arguments.length; argidx++ )
            {
                inside = arguments[argidx];
                tagobj._insides.push(inside);
            }

            tagobj._ft_render_ = function(stringPadding) {
                var html, idx, jdx, content, inside, value, attr, units, style, innerHTML, first_, innerPadding, skipNextPadding;

                // start tag and add all attributes, including decoding of styles
                style = '';
                html = '<' + this._ft_name;
                /*jslint forin:true */
                for ( attr in this._attributes )
                {
                    first_ = attr.indexOf('_'); // skip attributes that start with underscore
                    if ( first_ !== 0 )
                    {
                        value = this._attributes[attr];
                        if ( value !== undefined )  // if decodes as undefined then don't add this attribute
                        {
                            // sometimes there is a unit as part of the attr name
                            if ( -1 === first_ )
                            {
                                units = '';
                            }
                            else
                            {
                                jdx = attr.lastIndexOf('_');
                                units = attr.substring(jdx+1);
                                attr = attr.substring(0,jdx);
                                if ( units === 'pct') // because cannot have % be part of a name
                                {
                                    units = '%';
                                }
                            }

                            if ( is_array(value) )
                            {
                                // do not count any undefined values, so remove those from the array
                                for ( jdx = 0; jdx < value.length; jdx++ )
                                {
                                    if ( value[jdx] === undefined )
                                    {
                                        value.splice(jdx,1);
                                        jdx--;
                                    }
                                }
                                if ( value.length === 0 )
                                {
                                    // no values in the array, so don't output this attribute
                                    continue;
                                }
                                // if value is an array, turn it into a space-delimited string
                                value = value.join(units + ' ');
                            }
                            value += units;

                            if ( attr.substring(0,3) === 'css' )
                            {
                                style += camel_to_dash(attr).substring(4) + ':' + value + ';';
                            }
                            else
                            {
                                html += ' ' + attr + '="' + value + '"';
                            }
                        }
                    }
                }
                /*jslint forin:false */

                if ( !this._attributes._nobrout )
                {
                    html = stringPadding + html;
                }

                if ( (stringPadding.length === 0) && !this._attributes._nobrin )
                {
                    stringPadding = '\r\n';
                }

                if ( style.length !== 0 )
                {
                    html += ' style="' + style + '"';
                }

                innerHTML = '';
                innerPadding = this._attributes._nobrin ? '' : (stringPadding + '  ');
                skipNextPadding = false;
                for ( idx = 0; idx < this._insides.length; idx++ )
                {
                    inside = this._insides[idx];
                    if ( !is_array(inside) ) { inside = [inside]; }
                    for ( jdx = 0; jdx < inside.length; jdx++ )
                    {
                        content = inside[jdx];
                        if ( content !== undefined )
                        {
                            try {
                                innerHTML += content._ft_render_(skipNextPadding?'':innerPadding);
                                skipNextPadding = content._attributes._nobrout;
                            } catch(e) {
                                if ( !skipNextPadding )
                                {
                                    innerHTML += innerPadding;
                                }
                                else
                                {
                                    skipNextPadding = false;
                                }
                                innerHTML += String(content);
                            }
                        }
                    }
                }

                if ( (innerHTML.length === 0) && !(this._attributes._noslfcls) )
                {
                    html += '/>';
                }
                else
                {
                    html += '>' + innerHTML + ((this._attributes._nobrin) ? '' : stringPadding) + '</' + this._ft_name + '>';
                }
                return html;
            };

            tagobj.toString = function() { return this._ft_render_(''); };

            return tagobj;
        };
        tag._ft_name = name;
        tag.default_attributes = default_attributes;
        return tag;
    }

    /*************** initialize ***************/

    window.funcyTag = funcyTag;

    /*************** the "esc" feature ***************/

    window.funcyTag.esc = function( str )
    {
        return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
    };

}(window));
<|endoftext|>


<commit_before>'use strict';

import should from 'should';
import TrieNode from '../src/TrieNode';

const emptyTrie = new TrieNode();

describe('TrieNode', function () {

  describe('#_getChild', function () {
    it('returns the child if a child exists with the character ', function () {
      const node = new TrieNode();
      const child = new TrieNode();
      node.childrenByLeadingChar['a'] = child;
      node._getChild('a').should.equal(child);
    });

    it('returns the null if there is no child with the character', function () {
      const node = new TrieNode();
      const child = new TrieNode();
      node.childrenByLeadingChar['b'] = child;
      should.not.exist(node._getChild('a'));
    });
  });

  describe('#_getEdge', function () {
    it('returns the edge if an edge starts with the character ', function () {
      const node = new TrieNode();
      const child = new TrieNode();
      node.edgesByLeadingChar['a'] = 'abc';
      node._getEdge('a').should.equal('abc');
    });

    it('returns the null if no edge starts with the character', function () {
      const node = new TrieNode();
      const child = new TrieNode();
      node.edgesByLeadingChar['b'] = 'bcd';
      should.not.exist(node._getEdge('a'));
    });
  });

  describe('#_addChild', function () {
    it('should add a child', function () {
      const trie = new TrieNode(); // TODO
      trie.childrenByLeadingChar.should.match({});
    });
  });

  describe('#_matchUntilMismatch', function () {
    it('should return the current node and pattern if there is no match', function () {
      const trie = new TrieNode();
      const { lastNode, remainingPattern } = trie._matchUntilMismatch('ABC');
      lastNode.should.equal(trie);
      remainingPattern.should.equal('ABC');
    });
  });

  describe('#match', function () {
    it('should match an empty string', function () {
  	  emptyTrie.match('').should.be.exactly(emptyTrie);
    });

    it('should return false if the pattern does not match', function () {
      const trie = new TrieNode();
      trie.add('apple');
      trie.add('apricot');
      trie.add('banana');

      should.not.exist(trie.match('zebra'));
      should.not.exist(trie.match('pple'));
      should.not.exist(trie.match('ana'));
    });

    it('should match prefixes of strings in the trie', function () {
      const trie = new TrieNode();
      trie.add('ab', [1]);
      trie.add('ac', [1]);
      trie.add('ad', [3]);

      trie.match('ac').labels[1].should.equal(1);
    });

    it('should match strings contained in the trie', function () {
      const trie = new TrieNode();
      trie.add('apple$');
      trie.add('app$');
      trie.add('cat$');
      should.exist(trie.match('app$'));
    });

  });

  describe('#prettyPrint', function () {
    it('should return empty string when trie is empty', function () {
      const trie = new TrieNode();
      trie.prettyPrint().should.be.exactly('');
    });

    it('should print the trie', function () {
      const trie = new TrieNode();
      trie.add('abcd');
      trie.add('abce');

      trie.prettyPrint().should.be.exactly([
        '',
        '└─ abc ',
        '   ├─ d ',
        '   └─ e ',
      ].join('\n'));
    });
  });

  describe('#isLeaf', function () {
    it('should return false is the node has any children', function () {
      const rootNode = new TrieNode(),
        leftNode = new TrieNode(rootNode),
        rightNode = new TrieNode(rootNode);

      rootNode.childrenByLeadingChar['L'] = leftNode;
      rootNode.isLeaf().should.be.false();

      rootNode.childrenByLeadingChar['R'] = rightNode;
      rootNode.isLeaf().should.be.false();
    });

    it('should return true is the node has no children', function () {
      const rootNode = new TrieNode();
      rootNode.isLeaf().should.be.true();
    });
  });

  describe('#depth', function () {
    it('should return 0 for the root node', function () {
        const rootNode = new TrieNode();
        rootNode.depth().should.be.exactly(0);
    });

    it('should print the length of the longest path from the root to a leaf', function () {
      const rootNode = new TrieNode();
      rootNode.depth().should.be.exactly(0);

      const firstChild = new TrieNode({ rootNode: rootNode }. rootNode);
      rootNode.childrenByLeadingChar['a'] = firstChild;
      rootNode.depth().should.be.exactly(1);

      const secondChild = new TrieNode(rootNode);
      const childOfSecondChild = new TrieNode(secondChild);
      rootNode.childrenByLeadingChar['b'] = secondChild;
      secondChild.childrenByLeadingChar['c'] = childOfSecondChild;
      rootNode.depth().should.be.exactly(2);
    });

  });

  describe('#size', function () {
    it('should return 1 for a single node', function () {
        const rootNode = new TrieNode();
        rootNode.size().should.be.exactly(1);
    });

    it('should print the number of nodes in the tree', function () {
      const rootNode = new TrieNode();

      const firstChild = new TrieNode({ rootNode: rootNode }. rootNode);
      rootNode.childrenByLeadingChar['a'] = firstChild;

      const secondChild = new TrieNode(rootNode);
      const childOfSecondChild = new TrieNode(secondChild);
      rootNode.childrenByLeadingChar['b'] = secondChild;

      rootNode.size().should.be.exactly(3);
    });
  });

  describe('#getChildren', function () {
    it('should return an empty array if the node has no children', function () {
      const rootNode = new TrieNode();
      rootNode.getChildren().should.be.an.Array().and.be.empty();
    });

    it('should return an array of nodes', function () {
      const rootNode = new TrieNode();
      const nodeA = new TrieNode(rootNode);
      const nodeB = new TrieNode(rootNode);
      rootNode.childrenByLeadingChar['a'] = nodeA;
      rootNode.childrenByLeadingChar['b'] = nodeB;

      const nodeC = new TrieNode(nodeA);
      nodeA.childrenByLeadingChar['c'] = nodeC;

      const children = rootNode.getChildren();
      children.should.be.an.Array();
      children.should.have.length(2);
      children[0].should.equal(nodeA);
      children[1].should.equal(nodeB);
    });

  });

  describe('#hasEdgeWithPrefix', function () {
    it('returns false if there is no edge with the prefix', function () {
      const node = new TrieNode();
      node.edgesByLeadingChar['a'] = 'aaa';
      node.hasEdgeWithPrefix('aab').should.equal(false);
    });

    it('returns true if there is an edge with the prefix', function () {
      const node = new TrieNode();
      node.edgesByLeadingChar['a'] = 'aaa';
      node.hasEdgeWithPrefix('aaa').should.equal(true);
    });
  });

  describe('#_connectByEdge', function () {
    it('connects a child to a node by an edge', function () {
      const node = new TrieNode();
      const childToConnect = new TrieNode();
      node._connectByEdge(childToConnect, 'abcd');
      node._getChild('a').should.equal(childToConnect);
      node._getEdge('a').should.equal('abcd');
    });
  });

  describe('#leaves', function () {
    it('should return an array with just the node if the node is a leaf', function () {
      const rootNode = new TrieNode();
      const leaves = rootNode.leaves();
      leaves.should.be.an.Array();
      leaves.should.have.length(1);
      leaves[0].should.equal(rootNode);
    });

    it('should return an array of leaf nodes', function () {
      const rootNode = new TrieNode();
      const nodeA = new TrieNode(rootNode);
      const nodeB = new TrieNode(rootNode);
      rootNode.childrenByLeadingChar['a'] = nodeA;
      rootNode.childrenByLeadingChar['b'] = nodeB;

      const nodeC = new TrieNode(nodeA);
      nodeA.childrenByLeadingChar['c'] = nodeC;

      const leaves = rootNode.leaves();
      leaves.should.be.an.Array();
      leaves.should.have.length(2);
      leaves[0].should.equal(nodeB);
      leaves[1].should.equal(nodeC);
    });

  });

  describe('#add', function () {
    it('adds a string with the given label', function () {
      const node = new TrieNode();
      node.add('abc', ['LABEL']);
      node._getChild('a').labels['LABEL'].should.equal('LABEL');
    });
  })

  describe('#_addChild', function () {
    context('there are no children', function () {
      it('adds a child', function () {
        const rootNode = new TrieNode();
        rootNode._addChild('abc');
        rootNode.depth().should.equal(1);
        rootNode.edgesByLeadingChar['a'].should.equal('abc');
        should.exist(rootNode.childrenByLeadingChar['a']);
      });
    });

    context('there is a child that does not share a prefix', function () {
      it('adds a child', function () {
        const rootNode = new TrieNode();
        rootNode._addChild('abc');
        rootNode._addChild('def');
        rootNode.depth().should.equal(1);
        rootNode.edgesByLeadingChar['a'].should.equal('abc');
        should.exist(rootNode.childrenByLeadingChar['a']);

        rootNode.edgesByLeadingChar['d'].should.equal('def');
        should.exist(rootNode.childrenByLeadingChar['d']);
      });
    });

    context('there is a child that shares a prefix', function () {
      it('adds a child', function () {
        const rootNode = new TrieNode();
        rootNode._addChild('abc');
        rootNode._addChild('abd');
        rootNode.depth().should.equal(2);
        rootNode.edgesByLeadingChar['a'].should.equal('ab');
        should.exist(rootNode.childrenByLeadingChar['a']);

        const firstChild = rootNode.childrenByLeadingChar['a'];
        firstChild.edgesByLeadingChar['c'].should.equal('c');
        firstChild.edgesByLeadingChar['d'].should.equal('d');
      });
    });

  });

  describe('#getLabels', function () {
    it('returns an empty array if the node has no labels', function () {
      const node = new TrieNode();
      node.getLabels().should.match([]);
    });

    it('returns array of the nodes labels', function () {
      const node = new TrieNode();
      node.labels['abc'] = 'abc';
      node.labels['def'] = 'def';
      node.getLabels().should.match(['abc', 'def']);
    });
  });

  describe('#hasLabel', function () {
    it('returns false if the node does not have the label', function () {
      const node = new TrieNode();
      node.hasLabel(1).should.equal(false);
    });

    it('returns true if the node has the label', function () {
      const node = new TrieNode();
      node.labels[1] = 1;
      node.hasLabel(1).should.equal(true);
    });
  });

  describe('#getParent', function () {
    it('should return a childs parent', function () {
      const parent = new TrieNode();
      const child = parent._addChild('abc');
      child.getParent().should.equal(parent);
    });

    it('should return null if the node has no parent', function () {
      const root = new TrieNode();
      should.not.exist(root.getParent());
    });
  });

  describe('#disconnectChild', function () {
    it('disconnects a child from the parent', function () {
      const parent = new TrieNode();
      const child = parent._addChild('abc');
      parent.disconnectChild(child);
      parent.getChildren().should.be.empty();
    });
  });

  describe('#getParentEdge', function () {
    it('returns the edge which connects the node to its parent', function () {
      const parent = new TrieNode();
      const child = parent._addChild('abc');
      child.getParentEdge().should.equal('abc');
    });

    it('returns null if there is no parent', function () {
      const parent = new TrieNode();
      should.not.exist(parent.getParentEdge());
    });
  });

  describe('#removeAndPrune', function () {
    it('removes the node', function () {
      const node = new TrieNode();
      const child = node._addChild('abc');

      node.getChildren().length.should.equal(1);
      child.removeAndPrune();
      node.getChildren().should.be.empty();
    });

    it('compacts the trie', function () {
      const parent = new TrieNode();

      const child1 = parent._addChild('abc');
      const child2 = parent._addChild('abd');

      parent.size().should.equal(4);

      const leaf = parent.match('abd');
      leaf.removeAndPrune();

      parent.size().should.equal(2);
    });
  });
});
<commit_msg>add another test case<commit_after>'use strict';

import should from 'should';
import TrieNode from '../src/TrieNode';

const emptyTrie = new TrieNode();

describe('TrieNode', function () {

  describe('#_getChild', function () {
    it('returns the child if a child exists with the character ', function () {
      const node = new TrieNode();
      const child = new TrieNode();
      node.childrenByLeadingChar['a'] = child;
      node._getChild('a').should.equal(child);
    });

    it('returns the null if there is no child with the character', function () {
      const node = new TrieNode();
      const child = new TrieNode();
      node.childrenByLeadingChar['b'] = child;
      should.not.exist(node._getChild('a'));
    });
  });

  describe('#_getEdge', function () {
    it('returns the edge if an edge starts with the character ', function () {
      const node = new TrieNode();
      const child = new TrieNode();
      node.edgesByLeadingChar['a'] = 'abc';
      node._getEdge('a').should.equal('abc');
    });

    it('returns the null if no edge starts with the character', function () {
      const node = new TrieNode();
      const child = new TrieNode();
      node.edgesByLeadingChar['b'] = 'bcd';
      should.not.exist(node._getEdge('a'));
    });
  });

  describe('#_addChild', function () {
    it('should add a child', function () {
      const trie = new TrieNode(); // TODO
      trie.childrenByLeadingChar.should.match({});
    });
  });

  describe('#_matchUntilMismatch', function () {
    it('should return the current node and pattern if there is no match', function () {
      const trie = new TrieNode();
      const { lastNode, remainingPattern } = trie._matchUntilMismatch('ABC');
      lastNode.should.equal(trie);
      remainingPattern.should.equal('ABC');
    });

    it('should return the last matched node and remiaining pattern if there is a partial match', function () {
      const trie = new TrieNode();
      trie.add('aa');
      trie.add('ab');

      const { lastNode, remainingPattern } = trie._matchUntilMismatch('ac');
      lastNode.should.equal(trie._getChild('a'));
      remainingPattern.should.equal('c');
    });
  });

  describe('#match', function () {
    it('should match an empty string', function () {
  	  emptyTrie.match('').should.be.exactly(emptyTrie);
    });

    it('should return false if the pattern does not match', function () {
      const trie = new TrieNode();
      trie.add('apple');
      trie.add('apricot');
      trie.add('banana');

      should.not.exist(trie.match('zebra'));
      should.not.exist(trie.match('pple'));
      should.not.exist(trie.match('ana'));
    });

    it('should match prefixes of strings in the trie', function () {
      const trie = new TrieNode();
      trie.add('ab', [1]);
      trie.add('ac', [1]);
      trie.add('ad', [3]);

      trie.match('ac').labels[1].should.equal(1);
    });

    it('should match strings contained in the trie', function () {
      const trie = new TrieNode();
      trie.add('apple$');
      trie.add('app$');
      trie.add('cat$');
      should.exist(trie.match('app$'));
    });

  });

  describe('#prettyPrint', function () {
    it('should return empty string when trie is empty', function () {
      const trie = new TrieNode();
      trie.prettyPrint().should.be.exactly('');
    });

    it('should print the trie', function () {
      const trie = new TrieNode();
      trie.add('abcd');
      trie.add('abce');

      trie.prettyPrint().should.be.exactly([
        '',
        '└─ abc ',
        '   ├─ d ',
        '   └─ e ',
      ].join('\n'));
    });
  });

  describe('#isLeaf', function () {
    it('should return false is the node has any children', function () {
      const rootNode = new TrieNode(),
        leftNode = new TrieNode(rootNode),
        rightNode = new TrieNode(rootNode);

      rootNode.childrenByLeadingChar['L'] = leftNode;
      rootNode.isLeaf().should.be.false();

      rootNode.childrenByLeadingChar['R'] = rightNode;
      rootNode.isLeaf().should.be.false();
    });

    it('should return true is the node has no children', function () {
      const rootNode = new TrieNode();
      rootNode.isLeaf().should.be.true();
    });
  });

  describe('#depth', function () {
    it('should return 0 for the root node', function () {
        const rootNode = new TrieNode();
        rootNode.depth().should.be.exactly(0);
    });

    it('should print the length of the longest path from the root to a leaf', function () {
      const rootNode = new TrieNode();
      rootNode.depth().should.be.exactly(0);

      const firstChild = new TrieNode({ rootNode: rootNode }. rootNode);
      rootNode.childrenByLeadingChar['a'] = firstChild;
      rootNode.depth().should.be.exactly(1);

      const secondChild = new TrieNode(rootNode);
      const childOfSecondChild = new TrieNode(secondChild);
      rootNode.childrenByLeadingChar['b'] = secondChild;
      secondChild.childrenByLeadingChar['c'] = childOfSecondChild;
      rootNode.depth().should.be.exactly(2);
    });

  });

  describe('#size', function () {
    it('should return 1 for a single node', function () {
        const rootNode = new TrieNode();
        rootNode.size().should.be.exactly(1);
    });

    it('should print the number of nodes in the tree', function () {
      const rootNode = new TrieNode();

      const firstChild = new TrieNode({ rootNode: rootNode }. rootNode);
      rootNode.childrenByLeadingChar['a'] = firstChild;

      const secondChild = new TrieNode(rootNode);
      const childOfSecondChild = new TrieNode(secondChild);
      rootNode.childrenByLeadingChar['b'] = secondChild;

      rootNode.size().should.be.exactly(3);
    });
  });

  describe('#getChildren', function () {
    it('should return an empty array if the node has no children', function () {
      const rootNode = new TrieNode();
      rootNode.getChildren().should.be.an.Array().and.be.empty();
    });

    it('should return an array of nodes', function () {
      const rootNode = new TrieNode();
      const nodeA = new TrieNode(rootNode);
      const nodeB = new TrieNode(rootNode);
      rootNode.childrenByLeadingChar['a'] = nodeA;
      rootNode.childrenByLeadingChar['b'] = nodeB;

      const nodeC = new TrieNode(nodeA);
      nodeA.childrenByLeadingChar['c'] = nodeC;

      const children = rootNode.getChildren();
      children.should.be.an.Array();
      children.should.have.length(2);
      children[0].should.equal(nodeA);
      children[1].should.equal(nodeB);
    });

  });

  describe('#hasEdgeWithPrefix', function () {
    it('returns false if there is no edge with the prefix', function () {
      const node = new TrieNode();
      node.edgesByLeadingChar['a'] = 'aaa';
      node.hasEdgeWithPrefix('aab').should.equal(false);
    });

    it('returns true if there is an edge with the prefix', function () {
      const node = new TrieNode();
      node.edgesByLeadingChar['a'] = 'aaa';
      node.hasEdgeWithPrefix('aaa').should.equal(true);
    });
  });

  describe('#_connectByEdge', function () {
    it('connects a child to a node by an edge', function () {
      const node = new TrieNode();
      const childToConnect = new TrieNode();
      node._connectByEdge(childToConnect, 'abcd');
      node._getChild('a').should.equal(childToConnect);
      node._getEdge('a').should.equal('abcd');
    });
  });

  describe('#leaves', function () {
    it('should return an array with just the node if the node is a leaf', function () {
      const rootNode = new TrieNode();
      const leaves = rootNode.leaves();
      leaves.should.be.an.Array();
      leaves.should.have.length(1);
      leaves[0].should.equal(rootNode);
    });

    it('should return an array of leaf nodes', function () {
      const rootNode = new TrieNode();
      const nodeA = new TrieNode(rootNode);
      const nodeB = new TrieNode(rootNode);
      rootNode.childrenByLeadingChar['a'] = nodeA;
      rootNode.childrenByLeadingChar['b'] = nodeB;

      const nodeC = new TrieNode(nodeA);
      nodeA.childrenByLeadingChar['c'] = nodeC;

      const leaves = rootNode.leaves();
      leaves.should.be.an.Array();
      leaves.should.have.length(2);
      leaves[0].should.equal(nodeB);
      leaves[1].should.equal(nodeC);
    });

  });

  describe('#add', function () {
    it('adds a string with the given label', function () {
      const node = new TrieNode();
      node.add('abc', ['LABEL']);
      node._getChild('a').labels['LABEL'].should.equal('LABEL');
    });
  })

  describe('#_addChild', function () {
    context('there are no children', function () {
      it('adds a child', function () {
        const rootNode = new TrieNode();
        rootNode._addChild('abc');
        rootNode.depth().should.equal(1);
        rootNode.edgesByLeadingChar['a'].should.equal('abc');
        should.exist(rootNode.childrenByLeadingChar['a']);
      });
    });

    context('there is a child that does not share a prefix', function () {
      it('adds a child', function () {
        const rootNode = new TrieNode();
        rootNode._addChild('abc');
        rootNode._addChild('def');
        rootNode.depth().should.equal(1);
        rootNode.edgesByLeadingChar['a'].should.equal('abc');
        should.exist(rootNode.childrenByLeadingChar['a']);

        rootNode.edgesByLeadingChar['d'].should.equal('def');
        should.exist(rootNode.childrenByLeadingChar['d']);
      });
    });

    context('there is a child that shares a prefix', function () {
      it('adds a child', function () {
        const rootNode = new TrieNode();
        rootNode._addChild('abc');
        rootNode._addChild('abd');
        rootNode.depth().should.equal(2);
        rootNode.edgesByLeadingChar['a'].should.equal('ab');
        should.exist(rootNode.childrenByLeadingChar['a']);

        const firstChild = rootNode.childrenByLeadingChar['a'];
        firstChild.edgesByLeadingChar['c'].should.equal('c');
        firstChild.edgesByLeadingChar['d'].should.equal('d');
      });
    });

  });

  describe('#getLabels', function () {
    it('returns an empty array if the node has no labels', function () {
      const node = new TrieNode();
      node.getLabels().should.match([]);
    });

    it('returns array of the nodes labels', function () {
      const node = new TrieNode();
      node.labels['abc'] = 'abc';
      node.labels['def'] = 'def';
      node.getLabels().should.match(['abc', 'def']);
    });
  });

  describe('#hasLabel', function () {
    it('returns false if the node does not have the label', function () {
      const node = new TrieNode();
      node.hasLabel(1).should.equal(false);
    });

    it('returns true if the node has the label', function () {
      const node = new TrieNode();
      node.labels[1] = 1;
      node.hasLabel(1).should.equal(true);
    });
  });

  describe('#getParent', function () {
    it('should return a childs parent', function () {
      const parent = new TrieNode();
      const child = parent._addChild('abc');
      child.getParent().should.equal(parent);
    });

    it('should return null if the node has no parent', function () {
      const root = new TrieNode();
      should.not.exist(root.getParent());
    });
  });

  describe('#disconnectChild', function () {
    it('disconnects a child from the parent', function () {
      const parent = new TrieNode();
      const child = parent._addChild('abc');
      parent.disconnectChild(child);
      parent.getChildren().should.be.empty();
    });
  });

  describe('#getParentEdge', function () {
    it('returns the edge which connects the node to its parent', function () {
      const parent = new TrieNode();
      const child = parent._addChild('abc');
      child.getParentEdge().should.equal('abc');
    });

    it('returns null if there is no parent', function () {
      const parent = new TrieNode();
      should.not.exist(parent.getParentEdge());
    });
  });

  describe('#removeAndPrune', function () {
    it('removes the node', function () {
      const node = new TrieNode();
      const child = node._addChild('abc');

      node.getChildren().length.should.equal(1);
      child.removeAndPrune();
      node.getChildren().should.be.empty();
    });

    it('compacts the trie', function () {
      const parent = new TrieNode();

      const child1 = parent._addChild('abc');
      const child2 = parent._addChild('abd');

      parent.size().should.equal(4);

      const leaf = parent.match('abd');
      leaf.removeAndPrune();

      parent.size().should.equal(2);
    });
  });
});
<|endoftext|>


<commit_before>#! /usr/bin/env node

// General directives and requires

import {argv}   from 'yargs';
import handlers from './handlers';

// TODO: These variable names are awful; decide on better vocabulary
const command = argv._[0];
const jenType = argv._[1];
const jenName = argv._[2];

console.log('command:', command);
console.log('jenType:', jenType);
console.log('jenName:', jenName);

if ( command === 'jen' && jenType in handlers ) {
  console.log(handlers[jenType]);
  handlers[jenType](jenName);
} else {
  console.log(`No handler exists for command ${command} and type ${jenType}!`);
}

// TODO: add build task to restore backup package.json?<commit_msg>template strings<commit_after>#! /usr/bin/env node

// General directives and requires

import {argv}   from 'yargs';
import handlers from './handlers';

// TODO: These variable names are awful; decide on better vocabulary
const command = argv._[0];
const jenType = argv._[1];
const jenName = argv._[2];

console.log(`command: ${command}`);
console.log(`jenType: ${jenType}`);
console.log(`jenName: ${jenName}`);

if ( command === 'jen' && jenType in handlers ) {
  console.log(handlers[jenType]);
  handlers[jenType](jenName);
} else {
  console.log(`No handler exists for command ${command} and type ${jenType}!`);
}

// TODO: add build task to restore backup package.json?<|endoftext|>


<commit_before>'use strict';

/**
 * koa middleware to render view using Nunjucks.
 *
 * author: Michael Liao
 */

const
    nunjucks = require('nunjucks'),
    config = require('../config'),
    SECURE = config.session.https;

function createEnv(path, opts) {
    let
        autoescape = opts.autoescape === undefined ? true : opts.autoescape,
        noCache = opts.noCache || false,
        watch = opts.watch || false,
        throwOnUndefined = opts.throwOnUndefined || false,
        env = new nunjucks.Environment(
            new nunjucks.FileSystemLoader('views', {
                noCache: noCache,
                watch: watch,
            }), {
                autoescape: autoescape,
                throwOnUndefined: throwOnUndefined
            }),
        f;
    if (opts.filters) {
        for (f in opts.filters) {
            env.addFilter(f, opts.filters[f]);
        }
    }
    return env;
}

module.exports = (path, opts) => {
    let env = createEnv(path, opts);
    return async (ctx, next) => {
        ctx.render = function (view, model) {
            let
                viewPath = view,
                path = ctx.request.path;
            if (! path.startsWith('/manage/')) {
                viewPath = 'themes/' + ctx.state.__theme__ + '/' + view;
            }
            if (SECURE) {
                ctx.response.set('Content-Security-Policy', 'upgrade-insecure-requests');
            }
            ctx.response.type = 'text/html';
            ctx.response.body = env.render(viewPath, Object.assign({}, ctx.state || {}, model || {}));
        };
        await next();
    };
};
<commit_msg>fix view path<commit_after>'use strict';

/**
 * koa middleware to render view using Nunjucks.
 *
 * author: Michael Liao
 */

const
    nunjucks = require('nunjucks'),
    config = require('../config'),
    SECURE = config.session.https;

function createEnv(path, opts) {
    let
        autoescape = opts.autoescape === undefined ? true : opts.autoescape,
        noCache = opts.noCache || false,
        watch = opts.watch || false,
        throwOnUndefined = opts.throwOnUndefined || false,
        env = new nunjucks.Environment(
            new nunjucks.FileSystemLoader(path, {
                noCache: noCache,
                watch: watch,
            }), {
                autoescape: autoescape,
                throwOnUndefined: throwOnUndefined
            }),
        f;
    if (opts.filters) {
        for (f in opts.filters) {
            env.addFilter(f, opts.filters[f]);
        }
    }
    return env;
}

module.exports = (path, opts) => {
    let env = createEnv(path, opts);
    return async (ctx, next) => {
        ctx.render = function (view, model) {
            let
                viewPath = view,
                path = ctx.request.path;
            if (! path.startsWith('/manage/')) {
                viewPath = 'themes/' + ctx.state.__theme__ + '/' + view;
            }
            if (SECURE) {
                ctx.response.set('Content-Security-Policy', 'upgrade-insecure-requests');
            }
            ctx.response.type = 'text/html';
            ctx.response.body = env.render(viewPath, Object.assign({}, ctx.state || {}, model || {}));
        };
        await next();
    };
};
<|endoftext|>


<commit_before>/**
 * @preserve SCD.js
 *
 * Copyright 2011, Guillaume Marty (edo999@gmail.com)
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * https://github.com/gmarty/SCD.js
 */

/**
 * Perform a scene change detection process on a video tag.
 * @constructor
 * @param {HTMLVideoElement} videoEl The video element to process.
 * @param {Object.<string, *>=} options An array of options.
 * @param {function()=} callback The callback function executed when process is complete.
 */
var Scd = function(videoEl, options, callback) {
    if(!videoEl || videoEl.constructor.toString().indexOf("HTMLVideoElement") < 0) {
        throw "Inputed element is not a video element.";
    }else {
        videoEl = /** @type {HTMLVideoElement} */ (videoEl);
    }

    // Private properties.
    /**
     * @type {Scd}
     * @private
     */
    var that = this;

    /**
     * Default mode is FastForward. Playback mode is used on browsers that don't support setting current playback time to sub seconds (e.g. Opera).
     * @type {string}
     * @private
     */
    var _mode = "FastForwardMode";

    /**
     * The width at which the frames will be resized down to for comparison.
     * @type {number}
     * @private
     */
    var _step_width = 50;

    /**
     * The height at which the frames will be resized down to for comparison.
     * @type {number}
     * @private
     */
    var _step_height = 50;

    /**
     * The minimal duration of a scene in s. 2 consecutive scene changes can be detected within this interval.
     * @type {number}
     * @private
     */
    var _minSceneDuration = 0.25;

    /**
     * Percentage color difference above which a scene change is detected (0 <= threshold <= 100).
     * @type {number}
     * @private
     */
    var _threshold = 25;

    /**
     * Display detected scenes first frame.
     * @type {boolean}
     * @private
     */
    var _debug = false;

    /**
     * Maximum color difference possible.
     * @const
     * @type {number}
     */
    var maxDiff = Math.sqrt(255 * 255 * 3);

    /**
     * Maximum color difference possible / 100. Used to speed up calculations on debug.
     * @const
     * @type {number}
     */
    var maxDiff100 = maxDiff / 100;

    /**
     * Current playback time.
     * @type {number}
     * @private
     */
    var _currentTime = 0;

    /**
     * Last current playback time. Used in "PlaybackMode" only.
     * @type {number}
     * @private
     */
    var _lastCurrentTime = 0;

    /**
     * Video width.
     * @type {number}
     * @private
     */
    var _width = 0;

    /**
     * Video height.
     * @type {number}
     * @private
     */
    var _height = 0;

    /**
     * Initial state of controls attribute of the video tag.
     * @type {boolean}
     * @private
     */
    var _controls = videoEl.controls;

    /**
     * Create a new canvas element.
     * @return {HTMLCanvasElement} A new canvas element created.
     * @private
     */
    var createCanvas = function() {
        return /** @type {HTMLCanvasElement} */ (document.createElement("canvas"));
    };

    /**
     * Canvas element of image 1.
     * @type {HTMLCanvasElement}
     * @private
     */
    var _canvasA = createCanvas();

    /**
     * Canvas element of image 2.
     * @type {HTMLCanvasElement}
     * @private
     */
    var _canvasB = createCanvas();

    /**
     * Canvas context for image 1.
     * @type {Object}
     * @private
     */
    var _ctxA = _canvasA.getContext("2d");

    /**
     * Canvas context for image 2.
     * @type {Object}
     * @private
     */
    var _ctxB = _canvasB.getContext("2d");

    /**
     * Determine if current scene change detection process is stopped.
     * @type {boolean}
     * @private
     */
    var _stop = false;

    /**
     * The total number of zones in the video to process. Used to speed up calculation.
     * @const
     * @type {number}
     * @private
     */
    var _step_sq;

    /**
     * The div element to write debug into.
     * @type {Element}
     * @private
     */
    var _debugContainer;

    /**
     * The property holding the event emitter.
     * @type {Object}
     * @private
     */
    var _events;
    Scd.prototype["events"] = _events = new EventEmitter();

    /**
     * Expose data about the video when available.
     * @this {HTMLVideoElement}
     * @private
     */
    var getVideoData = function() {
        // durationchange appears to be the first event triggered by video that exposes width and height.
        _width = this.videoWidth;
        _height = this.videoHeight;
        _canvasA.width = _step_width;
        _canvasA.height = _step_height;
        _canvasB.width = _step_width;
        _canvasB.height = _step_height;
        //_ctxA.drawImage(this, 0, 0, _width, _height, 0, 0, _step_width, _step_height);

        this.removeEventListener("durationchange", getVideoData, false);
    };

    /**
     * Initialize values.
     * @private
     */
    var init = function() {
        // Options.
        if(options) {
            if(options["mode"] && options["mode"] === "PlaybackMode") {
                _mode = /** @type {string} */ (options["mode"]);
            }
            if(options["step_width"] && options["step_height"]) {
                _step_width = parseInt(options["step_width"], 10);
                _step_height = parseInt(options["step_height"], 10);
            }else if(options["step"]) {
                _step_width = parseInt(options["step"], 10);
                _step_height = parseInt(options["step"], 10);
            }
            if(options["minSceneDuration"]) {
                _minSceneDuration = parseFloat(options["minSceneDuration"]);
            }
            if(options["threshold"]) {
                _threshold = parseFloat(options["threshold"]);
            }
            if(options["debug"]) {
                _debug = Boolean(options["debug"]);
            }
            _lastCurrentTime = _minSceneDuration;
        }
        // _threshold is set between 0 and maxDiff100 interval to save calculations later.
        _threshold = _threshold * maxDiff100;
        // The number of pixels of resized frames. Used to speed up average calculation.
        _step_sq = _step_width * _step_height;

        // Debug
        if(_debug) {
            _debugContainer = document.createElement("div");
            _debugContainer.className = "scd-debug";
            document.getElementsByTagName("body")[0].appendChild(_debugContainer);
        }

        // @todo: Call this function if Scd is instantiated after durationchange was triggered.
        videoEl.addEventListener("durationchange", getVideoData, false);

        /**
         * Launch the scene detection process.
         */
        Scd.prototype["start"] = (_mode === "FastForwardMode") ? function() {
            // Fast forward mode.
            if(_stop) {
                return;
            }

            // Remove controls from video during process.
//////////////////////////////////////////////////////////////////////////////////////////////////videoEl.controls = false;

            videoEl.currentTime = _currentTime;
            videoEl.addEventListener("seeked", fastForwardModeEvent, false);

            detectSceneChange();
        } : function() {
            // Playback mode.
            if(_stop) {
                return;
            }

            // Remove controls from video during process.
//////////////////////////////////////////////////////////////////////////////////////////////////videoEl.controls = false;

            videoEl.currentTime = 0;
            videoEl.addEventListener("timeupdate", playbackModeEvent, false);
            videoEl.addEventListener("ended", videoEndedEvent, false);

            videoEl.play();
        };

        getMedian = (_step_sq % 2) ? function(numArray) {
            numArray.sort(compare);
            return numArray[((_step_sq + 1) / 2) - 1];
        } : function(numArray) {
            numArray.sort(compare);
            var middle = (_step_sq + 1) / 2;
            return (numArray[middle - 1.5] + numArray[middle - 0.5]) / 2;
        };
    };

    /**
     * Function triggered by seeked event on FastForwardMode.
     * @this {HTMLVideoElement}
     * @private
     */
    var fastForwardModeEvent = function() {
        detectSceneChange();

        _currentTime += _minSceneDuration;
        this.currentTime = _currentTime;
    };

    /**
     * Function triggered by timeupdate event on PlaybackMode.
     * @this {HTMLVideoElement}
     * @private
     */
    var playbackModeEvent = function() {
        if(this.currentTime - _lastCurrentTime >= _minSceneDuration) {
            _currentTime = this.currentTime;
            detectSceneChange();

            _lastCurrentTime = this.currentTime;
        }
    };

    /** Function called when video ends.
     * @type {function()}
     * @private
     */
    var videoEndedEvent = function() {
        if(callback) {
            callback();
        }
        that["stop"]();
        // @todo: Detach event when in PlaybackMode.
    };

    /**
     * Determine if a scene change occurred between last and current playback time.
     * @private
     */
    var detectSceneChange = function() {
        if(_stop) {
            return;
        }

        // @fixme: Bug on Opera. duration is not always defined.
        if(_mode === "FastForwardMode" && (videoEl.ended || _currentTime > videoEl.duration)) {
            videoEndedEvent();
            return;
        }

        _ctxA.drawImage(videoEl, 0, 0, _width, _height, 0, 0, _step_width, _step_height);
        var diff = computeDifferences(_ctxA, _ctxB);

        if(diff[0] > _threshold) {

            // Trigger a cut event.
            _events.emit("scenecut");

            that["sceneTimecodes"].push(_currentTime);
            if(_debug) {
                var /** @type {Element} */ tmpContainer = document.createElement("div"),
                    /** @type {HTMLCanvasElement} */ tmpCanvasA = createCanvas(),
                    /** @type {number} */ half_width = tmpCanvasA.width = _width / 2,
                    /** @type {number} */ half_height = tmpCanvasA.height = _height / 2;
                tmpCanvasA.getContext("2d").drawImage(videoEl, 0, 0, _width, _height, 0, 0, half_width, half_height);
                tmpContainer.appendChild(document.createTextNode("@ "+_currentTime));
                tmpContainer.appendChild(document.createElement("br"));
                tmpContainer.appendChild(tmpCanvasA);
                tmpContainer.appendChild(document.createElement("br"));
                tmpContainer.appendChild(document.createTextNode("max: " + Math.round(diff[2] / maxDiff100) + "%, avg: " + Math.round(diff[1] / maxDiff100) + "%, med: " + Math.round(diff[0] / maxDiff100) + "%, min: " + Math.round(diff[3] / maxDiff100) + "%"));
                _debugContainer.appendChild(tmpContainer);
            }
        }

        _ctxB.drawImage(_canvasA, 0, 0, _step_width, _step_height, 0, 0, _step_width, _step_height);
    };

    /**
     * Return various statistics about zone differences of 2 frames.
     * Debug mode computes and returns more data.
     * @param {Object} ctxA The canvas context of image 1.
     * @param {Object} ctxB The canvas context of image 2.
     * @return {Array.<number>} Various statistics about input image differences.
     * @private
     */
    var computeDifferences = function(ctxA, ctxB) {
        var /** @type {Array.<number>} */ diff = [],
            /** @type {Array.<number>} */ colorsA = ctxA.getImageData(0, 0, _step_width, _step_height).data,
            /** @type {Array.<number>} */ colorsB = ctxB.getImageData(0, 0, _step_width, _step_height).data,
            /** @type {number} */ i = colorsA.length,
            /** @type {number} */ max,
            /** @type {number} */ avg,
            /** @type {number} */ med,
            /** @type {number} */ min;

        do {
            diff.push(getColorDistance(colorsA[i-4], colorsA[i+(1-4)], colorsA[i+(2-4)], colorsB[i-4], colorsB[i+(1-4)], colorsB[i+(2-4)]));
        } while(i = i - 4);

        med = getMedian(diff);
        if(_debug) {
            // When debug is on, full data are computed and returned...
            avg = getAverage(diff);
            max = getMaxOfArray(diff);
            min = getMinOfArray(diff);
            return [med, avg, max, min];
        }else {
            // Otherwise, only the median value is returned.
            return [med];
        }
    };

    /**
     * Calculates the distance between 2 colors RGB compounds.
     * @param {number} RA Red compound value of color A.
     * @param {number} GA Green compound value of color A.
     * @param {number} BA Blue compound value of color A.
     * @param {number} RB Red compound value of color B.
     * @param {number} GB Green compound value of color B.
     * @param {number} BB Blue compound value of color B.
     * @return {number} The distance.
     */
    var getColorDistance = function(RA, GA, BA, RB, GB, BB) {
        return Math.sqrt(Math.pow(RA - RB, 2) + Math.pow(GA - GB, 2) + Math.pow(BA - BB, 2));
        //return Math.sqrt((RA - RB) * (RA - RB) + (GA - GB) * (GA - GB) + (BA - BB) * (BA - BB));
    };

    /**
     * Calculates the maximum value of an array.
     * @param {Array.<number>} numArray An array of values.
     * @return {number} The maximum value.
     */
    var getMaxOfArray = function(numArray) {
        return Math.max.apply(null, numArray);
    };

    /**
     * Calculates the minimum value of an array.
     * @param {Array.<number>} numArray An array of values.
     * @return {number} The minimum value.
     */
    var getMinOfArray = function(numArray) {
        return Math.min.apply(null, numArray);
    };

    /**
     * Calculates the average value of an array.
     * @param {Array.<number>} numArray An array of values.
     * @return {number} The average value.
     */
    var getAverage = function(numArray) {
        return numArray.reduce(function(a, b) {
            return a + b;
        }) / _step_sq;
    };

    /**
     * Calculates the median value of an array.
     * @param {Array.<number>} numArray An array of values.
     * @return {number} The median value.
     * @private
     */
    var getMedian;

    /**
     * Comparison function for Array.sort() used in getMedian().
     * @param {number} a The first value to compare.
     * @param {number} b The second value to compare.
     * @return {number} The difference between a and b.
     */
    var compare = function(a, b) {
        return a - b;
    };

    init();

    /**
     * Temporary halt the scene detection process. Use Scd.start() again to resume process.
     */
    Scd.prototype["pause"] = function() {
        if(_stop) {
            return;
        }

        if(_mode === "FastForwardMode") {
            // Restore video element controls to its original state.
            videoEl.controls = _controls;
            videoEl.removeEventListener("seeked", fastForwardModeEvent, false);
        }
        videoEl.pause();
    };

    /**
     * Cancel the scene detection process.
     */
    Scd.prototype["stop"] = function() {
        that["pause"]();

        if(_mode === "FastForwardMode") {
            // Restore video element controls to its original state.
            videoEl.controls = _controls;
        }

        _stop = true;
    };
};

/**
 * Contains detected scene changes timecodes.
 * @type {Array.<number>}
 * @public
 */
Scd.prototype["sceneTimecodes"] = [];
<commit_msg>Fix video controls<commit_after>/**
 * @preserve SCD.js
 *
 * Copyright 2011, Guillaume Marty (edo999@gmail.com)
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * https://github.com/gmarty/SCD.js
 */

/**
 * Perform a scene change detection process on a video tag.
 * @constructor
 * @param {HTMLVideoElement} videoEl The video element to process.
 * @param {Object.<string, *>=} options An array of options.
 * @param {function()=} callback The callback function executed when process is complete.
 */
var Scd = function(videoEl, options, callback) {
    if(!videoEl || videoEl.constructor.toString().indexOf("HTMLVideoElement") < 0) {
        throw "Inputed element is not a video element.";
    }else {
        videoEl = /** @type {HTMLVideoElement} */ (videoEl);
    }

    // Private properties.
    /**
     * @type {Scd}
     * @private
     */
    var that = this;

    /**
     * Default mode is FastForward. Playback mode is used on browsers that don't support setting current playback time to sub seconds (e.g. Opera).
     * @type {string}
     * @private
     */
    var _mode = "FastForwardMode";

    /**
     * The width at which the frames will be resized down to for comparison.
     * @type {number}
     * @private
     */
    var _step_width = 50;

    /**
     * The height at which the frames will be resized down to for comparison.
     * @type {number}
     * @private
     */
    var _step_height = 50;

    /**
     * The minimal duration of a scene in s. 2 consecutive scene changes can be detected within this interval.
     * @type {number}
     * @private
     */
    var _minSceneDuration = 0.25;

    /**
     * Percentage color difference above which a scene change is detected (0 <= threshold <= 100).
     * @type {number}
     * @private
     */
    var _threshold = 25;

    /**
     * Display detected scenes first frame.
     * @type {boolean}
     * @private
     */
    var _debug = false;

    /**
     * Maximum color difference possible.
     * @const
     * @type {number}
     */
    var maxDiff = Math.sqrt(255 * 255 * 3);

    /**
     * Maximum color difference possible / 100. Used to speed up calculations on debug.
     * @const
     * @type {number}
     */
    var maxDiff100 = maxDiff / 100;

    /**
     * Current playback time.
     * @type {number}
     * @private
     */
    var _currentTime = 0;

    /**
     * Last current playback time. Used in "PlaybackMode" only.
     * @type {number}
     * @private
     */
    var _lastCurrentTime = 0;

    /**
     * Video width.
     * @type {number}
     * @private
     */
    var _width = 0;

    /**
     * Video height.
     * @type {number}
     * @private
     */
    var _height = 0;

    /**
     * Initial state of controls attribute of the video tag.
     * @type {boolean}
     * @private
     */
    var _controls = videoEl.controls;

    /**
     * Create a new canvas element.
     * @return {HTMLCanvasElement} A new canvas element created.
     * @private
     */
    var createCanvas = function() {
        return /** @type {HTMLCanvasElement} */ (document.createElement("canvas"));
    };

    /**
     * Canvas element of image 1.
     * @type {HTMLCanvasElement}
     * @private
     */
    var _canvasA = createCanvas();

    /**
     * Canvas element of image 2.
     * @type {HTMLCanvasElement}
     * @private
     */
    var _canvasB = createCanvas();

    /**
     * Canvas context for image 1.
     * @type {Object}
     * @private
     */
    var _ctxA = _canvasA.getContext("2d");

    /**
     * Canvas context for image 2.
     * @type {Object}
     * @private
     */
    var _ctxB = _canvasB.getContext("2d");

    /**
     * Determine if current scene change detection process is stopped.
     * @type {boolean}
     * @private
     */
    var _stop = false;

    /**
     * The total number of zones in the video to process. Used to speed up calculation.
     * @const
     * @type {number}
     * @private
     */
    var _step_sq;

    /**
     * The div element to write debug into.
     * @type {Element}
     * @private
     */
    var _debugContainer;

    /**
     * The property holding the event emitter.
     * @type {Object}
     * @private
     */
    var _events;
    Scd.prototype["events"] = _events = new EventEmitter();

    /**
     * Expose data about the video when available.
     * @this {HTMLVideoElement}
     * @private
     */
    var getVideoData = function() {
        // durationchange appears to be the first event triggered by video that exposes width and height.
        _width = this.videoWidth;
        _height = this.videoHeight;
        _canvasA.width = _step_width;
        _canvasA.height = _step_height;
        _canvasB.width = _step_width;
        _canvasB.height = _step_height;
        //_ctxA.drawImage(this, 0, 0, _width, _height, 0, 0, _step_width, _step_height);

        this.removeEventListener("durationchange", getVideoData, false);
    };

    /**
     * Initialize values.
     * @private
     */
    var init = function() {
        // Options.
        if(options) {
            if(options["mode"] && options["mode"] === "PlaybackMode") {
                _mode = /** @type {string} */ (options["mode"]);
            }
            if(options["step_width"] && options["step_height"]) {
                _step_width = parseInt(options["step_width"], 10);
                _step_height = parseInt(options["step_height"], 10);
            }else if(options["step"]) {
                _step_width = parseInt(options["step"], 10);
                _step_height = parseInt(options["step"], 10);
            }
            if(options["minSceneDuration"]) {
                _minSceneDuration = parseFloat(options["minSceneDuration"]);
            }
            if(options["threshold"]) {
                _threshold = parseFloat(options["threshold"]);
            }
            if(options["debug"]) {
                _debug = Boolean(options["debug"]);
            }
            _lastCurrentTime = _minSceneDuration;
        }
        // _threshold is set between 0 and maxDiff100 interval to save calculations later.
        _threshold = _threshold * maxDiff100;
        // The number of pixels of resized frames. Used to speed up average calculation.
        _step_sq = _step_width * _step_height;

        // Debug
        if(_debug) {
            _debugContainer = document.createElement("div");
            _debugContainer.className = "scd-debug";
            document.getElementsByTagName("body")[0].appendChild(_debugContainer);
        }

        // @todo: Call this function if Scd is instantiated after durationchange was triggered.
        videoEl.addEventListener("durationchange", getVideoData, false);

        /**
         * Launch the scene detection process.
         */
        Scd.prototype["start"] = (_mode === "FastForwardMode") ? function() {
            // Fast forward mode.
            if(_stop) {
                return;
            }

            // Remove controls from video during process.
            videoEl.controls = false;

            videoEl.currentTime = _currentTime;
            videoEl.addEventListener("seeked", fastForwardModeEvent, false);

            detectSceneChange();
        } : function() {
            // Playback mode.
            if(_stop) {
                return;
            }

            // Remove controls from video during process.
            videoEl.controls = false;

            videoEl.currentTime = 0;
            videoEl.addEventListener("timeupdate", playbackModeEvent, false);
            videoEl.addEventListener("ended", videoEndedEvent, false);

            videoEl.play();
        };

        getMedian = (_step_sq % 2) ? function(numArray) {
            numArray.sort(compare);
            return numArray[((_step_sq + 1) / 2) - 1];
        } : function(numArray) {
            numArray.sort(compare);
            var middle = (_step_sq + 1) / 2;
            return (numArray[middle - 1.5] + numArray[middle - 0.5]) / 2;
        };
    };

    /**
     * Function triggered by seeked event on FastForwardMode.
     * @this {HTMLVideoElement}
     * @private
     */
    var fastForwardModeEvent = function() {
        detectSceneChange();

        _currentTime += _minSceneDuration;
        this.currentTime = _currentTime;
    };

    /**
     * Function triggered by timeupdate event on PlaybackMode.
     * @this {HTMLVideoElement}
     * @private
     */
    var playbackModeEvent = function() {
        if(this.currentTime - _lastCurrentTime >= _minSceneDuration) {
            _currentTime = this.currentTime;
            detectSceneChange();

            _lastCurrentTime = this.currentTime;
        }
    };

    /** Function called when video ends.
     * @type {function()}
     * @private
     */
    var videoEndedEvent = function() {
        if(callback) {
            callback();
        }
        that["stop"]();
        // @todo: Detach event when in PlaybackMode.
    };

    /**
     * Determine if a scene change occurred between last and current playback time.
     * @private
     */
    var detectSceneChange = function() {
        if(_stop) {
            return;
        }

        // @fixme: Bug on Opera. duration is not always defined.
        if(_mode === "FastForwardMode" && (videoEl.ended || _currentTime > videoEl.duration)) {
            videoEndedEvent();
            return;
        }

        _ctxA.drawImage(videoEl, 0, 0, _width, _height, 0, 0, _step_width, _step_height);
        var diff = computeDifferences(_ctxA, _ctxB);

        if(diff[0] > _threshold) {

            // Trigger a cut event.
            _events.emit("scenecut");

            that["sceneTimecodes"].push(_currentTime);
            if(_debug) {
                var /** @type {Element} */ tmpContainer = document.createElement("div"),
                    /** @type {HTMLCanvasElement} */ tmpCanvasA = createCanvas(),
                    /** @type {number} */ half_width = tmpCanvasA.width = _width / 2,
                    /** @type {number} */ half_height = tmpCanvasA.height = _height / 2;
                tmpCanvasA.getContext("2d").drawImage(videoEl, 0, 0, _width, _height, 0, 0, half_width, half_height);
                tmpContainer.appendChild(document.createTextNode("@ "+_currentTime));
                tmpContainer.appendChild(document.createElement("br"));
                tmpContainer.appendChild(tmpCanvasA);
                tmpContainer.appendChild(document.createElement("br"));
                tmpContainer.appendChild(document.createTextNode("max: " + Math.round(diff[2] / maxDiff100) + "%, avg: " + Math.round(diff[1] / maxDiff100) + "%, med: " + Math.round(diff[0] / maxDiff100) + "%, min: " + Math.round(diff[3] / maxDiff100) + "%"));
                _debugContainer.appendChild(tmpContainer);
            }
        }

        _ctxB.drawImage(_canvasA, 0, 0, _step_width, _step_height, 0, 0, _step_width, _step_height);
    };

    /**
     * Return various statistics about zone differences of 2 frames.
     * Debug mode computes and returns more data.
     * @param {Object} ctxA The canvas context of image 1.
     * @param {Object} ctxB The canvas context of image 2.
     * @return {Array.<number>} Various statistics about input image differences.
     * @private
     */
    var computeDifferences = function(ctxA, ctxB) {
        var /** @type {Array.<number>} */ diff = [],
            /** @type {Array.<number>} */ colorsA = ctxA.getImageData(0, 0, _step_width, _step_height).data,
            /** @type {Array.<number>} */ colorsB = ctxB.getImageData(0, 0, _step_width, _step_height).data,
            /** @type {number} */ i = colorsA.length,
            /** @type {number} */ max,
            /** @type {number} */ avg,
            /** @type {number} */ med,
            /** @type {number} */ min;

        do {
            diff.push(getColorDistance(colorsA[i-4], colorsA[i+(1-4)], colorsA[i+(2-4)], colorsB[i-4], colorsB[i+(1-4)], colorsB[i+(2-4)]));
        } while(i = i - 4);

        med = getMedian(diff);
        if(_debug) {
            // When debug is on, full data are computed and returned...
            avg = getAverage(diff);
            max = getMaxOfArray(diff);
            min = getMinOfArray(diff);
            return [med, avg, max, min];
        }else {
            // Otherwise, only the median value is returned.
            return [med];
        }
    };

    /**
     * Calculates the distance between 2 colors RGB compounds.
     * @param {number} RA Red compound value of color A.
     * @param {number} GA Green compound value of color A.
     * @param {number} BA Blue compound value of color A.
     * @param {number} RB Red compound value of color B.
     * @param {number} GB Green compound value of color B.
     * @param {number} BB Blue compound value of color B.
     * @return {number} The distance.
     */
    var getColorDistance = function(RA, GA, BA, RB, GB, BB) {
        return Math.sqrt(Math.pow(RA - RB, 2) + Math.pow(GA - GB, 2) + Math.pow(BA - BB, 2));
        //return Math.sqrt((RA - RB) * (RA - RB) + (GA - GB) * (GA - GB) + (BA - BB) * (BA - BB));
    };

    /**
     * Calculates the maximum value of an array.
     * @param {Array.<number>} numArray An array of values.
     * @return {number} The maximum value.
     */
    var getMaxOfArray = function(numArray) {
        return Math.max.apply(null, numArray);
    };

    /**
     * Calculates the minimum value of an array.
     * @param {Array.<number>} numArray An array of values.
     * @return {number} The minimum value.
     */
    var getMinOfArray = function(numArray) {
        return Math.min.apply(null, numArray);
    };

    /**
     * Calculates the average value of an array.
     * @param {Array.<number>} numArray An array of values.
     * @return {number} The average value.
     */
    var getAverage = function(numArray) {
        return numArray.reduce(function(a, b) {
            return a + b;
        }) / _step_sq;
    };

    /**
     * Calculates the median value of an array.
     * @param {Array.<number>} numArray An array of values.
     * @return {number} The median value.
     * @private
     */
    var getMedian;

    /**
     * Comparison function for Array.sort() used in getMedian().
     * @param {number} a The first value to compare.
     * @param {number} b The second value to compare.
     * @return {number} The difference between a and b.
     */
    var compare = function(a, b) {
        return a - b;
    };

    init();

    /**
     * Temporary halt the scene detection process. Use Scd.start() again to resume process.
     */
    Scd.prototype["pause"] = function() {
        if(_stop) {
            return;
        }

        if(_mode === "FastForwardMode") {
            // Restore video element controls to its original state.
            videoEl.controls = _controls;
            videoEl.removeEventListener("seeked", fastForwardModeEvent, false);
        }
        videoEl.pause();
    };

    /**
     * Cancel the scene detection process.
     */
    Scd.prototype["stop"] = function() {
        that["pause"]();

        if(_mode === "FastForwardMode") {
            // Restore video element controls to its original state.
            videoEl.controls = _controls;
        }

        _stop = true;
    };
};

/**
 * Contains detected scene changes timecodes.
 * @type {Array.<number>}
 * @public
 */
Scd.prototype["sceneTimecodes"] = [];
<|endoftext|>